diff --git a/README.md b/README.md index 3b5bee34..3e8171f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -> [!NOTE] -> Exciting news! Version 0.4 is coming soon, Backward compatible, with optional AI upgrade assistant (0.3.x to 0.4.x) for a smooth ride. -
Nextpy Logo @@ -37,14 +34,11 @@


- -Discord - ![-----------------------------------------------------](https://res.cloudinary.com/dzznkbdrb/image/upload/v1694798498/divider_1_rej288.gif) -

Streamlit's simplicity (but 4-10x faster) + FastAPI's full power + (Pydantic & SQL Alchemy)'s robustness

+

Build ⚡Blazing Fast, 🤖Self-Modifying Apps in Pure Python!

```diff @@ -53,9 +47,9 @@
-## 🤩 Nextpy : Fast, Pure Pythonic Web Apps +## 🤩 Nextpy : Build apps that write themselves -Build any web app —effortlessly and quickly ⚡. It simplifies Pythonic development for everything from backends to frontends (yes, visually 🦚stunning frontends in Python!), AI integrations, APIs, and beyond—empowering both humans and AI agents. +Build apps —effortlessly and quickly ⚡. It simplifies Pythonic development for everything from backends to frontends (yes, visually 🦚stunning frontends in Python!), AI integrations, APIs, and beyond—empowering both humans and AI agents. Nextpy is designed to solve compatibility issues and improve code generation. It is built on our insight🔍 that the choice of underlying frameworks significantly affects the efficiency of AI code generation, regardless of other factors such as LLMs, prompts, or fine-tuning methods. @@ -314,9 +308,9 @@ Initially, reflex's flexibility seemed promising, aligning with several of our r For months, we resisted the urge to reinvent the wheel and tried to assemble different tools to bring our vision to life. Instead of creating a framework from scratch, we aimed to create an opinionated app starter kit by selectively incorporating the best features from top frameworks. Although we still view this project as a **full stack app starter kit** rather than a framework, the distinction is becoming somewhat blurred as we have developed several custom modules and made specific design decisions. -This novel framework merges the simplicity of Streamlit with the speed and adaptability of Next.js. For the backend, we opted for FastAPI for its simplicity, coupled with Pydantic for robust type-checking. Our frontend was crafted using a tailored version of the Reflex library, incorporating new components. Currently, we are in the process of overhauling the core Python-to-JavaScript compiler to optimize speed. Additionally, we embraced SQLModel & SQLAlchemy for database connectivity. Furthermore, we have developed selected features specifically for generative AI. We have created a JSON-based database that uses JSON as the data storage medium, while also providing SQL-like capabilities. +This framework combines Streamlit's simplicity with Next.js's speed and flexibility. For the backend, FastAPI's ease of use is enhanced by Pydantic's strong type-checking. The frontend utilizes Reflex, Reacton, and Solara libraries, enabling not just web app support but also GUI integration within Jupyter, adding new components seamlessly. We're currently refining our Python-to-JavaScript compiler for improved speed. SQLModel and SQLAlchemy have been integrated for database management. Additionally, we've tailored features for generative AI and introduced a JSON-based database, offering SQL-like functionalities with JSON as the storage format. -**Our mission?** Make Next.py the most efficient app framework, for humans and AI alike! +**Our mission?** Make Next.py the most efficient app framework, for building self writing applications! Want to know more? Check out our manifesto! https://nextpy.org/manifesto/ diff --git a/app-examples/gallery/.gitignore b/app-examples/gallery/.gitignore index eab0d4b0..e2a72b70 100644 --- a/app-examples/gallery/.gitignore +++ b/app-examples/gallery/.gitignore @@ -1,4 +1,7 @@ *.db + *.py[cod] -.web + +*.web + __pycache__/ \ No newline at end of file diff --git a/app-examples/mapping/mapping/mapping.py b/app-examples/mapping/mapping/mapping.py index 4c82a217..f8835f90 100644 --- a/app-examples/mapping/mapping/mapping.py +++ b/app-examples/mapping/mapping/mapping.py @@ -3,7 +3,7 @@ from typing import Dict, List, Tuple import nextpy as xt -from nextpy.frontend.components.leaflet import ( +from nextpy.interfaces.web.components.leaflet import ( map_container, tile_layer, marker, diff --git a/nextpy/__init__.py b/nextpy/__init__.py index 2d8a537a..4201a02a 100644 --- a/nextpy/__init__.py +++ b/nextpy/__init__.py @@ -12,10 +12,12 @@ import importlib from typing import Type -from nextpy.frontend.page import page as page +from nextpy.interfaces.web.page import page as page from nextpy.utils import console from nextpy.utils.format import to_snake_case + + _ALL_COMPONENTS = [ "Accordion", "AccordionButton", @@ -61,6 +63,7 @@ "ColorModeButton", "ColorModeIcon", "ColorModeSwitch", + "ColorPicker", "Component", "Cond", "ConnectionBanner", @@ -86,6 +89,12 @@ "EditableTextarea", "Editor", "Email", + "Error", + "Expander", + "ExpanderButton", + "ExpanderIcon", + "ExpanderItem", + "ExpanderPanel", "Fade", "Flex", "Foreach", @@ -97,6 +106,7 @@ "Fragment", "Grid", "GridItem", + "Header", "Heading", "Highlight", "Hstack", @@ -104,6 +114,7 @@ "Icon", "IconButton", "Image", + "Info", "Input", "InputGroup", "InputLeftAddon", @@ -158,12 +169,12 @@ "PopoverHeader", "PopoverTrigger", "Progress", - "Radio", - "RadioGroup", "RangeSlider", + "RangeSliderTrack", "RangeSliderFilledTrack", "RangeSliderThumb", - "RangeSliderTrack", + "Radio", + "RadioGroup", "ResponsiveGrid", "ScaleFade", "Script", @@ -198,6 +209,7 @@ "StepStatus", "StepTitle", "Stepper", + "Success", "Switch", "Tab", "TabList", @@ -219,6 +231,7 @@ "Tfoot", "Th", "Thead", + "Title", "Tooltip", "Tr", "UnorderedList", @@ -226,6 +239,7 @@ "Video", "VisuallyHidden", "Vstack", + "Warning", "Wrap", "WrapItem", ] @@ -246,6 +260,12 @@ "EditorButtonList", "EditorOptions", "NoSSRComponent", + "dataframe", + "empty", + 'select_slider', + 'select_slider_filled_track', + 'select_slider_thumb', + 'select_slider_track' ] # _MAPPING: Maps module paths as keys to lists of their attributes (classes, functions, variables) as values for dynamic imports. @@ -283,14 +303,15 @@ "nextpy.constants": ["Env", "constants"], "nextpy.data.jsondb": ["JsonDatabase"], "nextpy.data.model": ["Model", "model", "session"], - "nextpy.frontend.components": _ALL_COMPONENTS + ["chakra", "next"], - "nextpy.frontend.components.framer.motion": ["motion"], - "nextpy.frontend.components.component": ["memo"], - "nextpy.frontend.components.el": ["el"], - "nextpy.frontend.components.moment.moment": ["MomentDelta"], - "nextpy.frontend.page": ["page"], - "nextpy.frontend.style": ["color_mode", "style", "toggle_color_mode"], - "nextpy.frontend.components.recharts": [ + "nextpy.interfaces.web.components": _ALL_COMPONENTS + ["chakra", "next"], + "nextpy.interfaces.web.components.framer.motion": ["motion"], + "nextpy.interfaces.web.components.component": ["memo"], + "nextpy.interfaces.web.components.el": ["el"], + "nextpy.interfaces.web.components.moment.moment": ["MomentDelta"], + "nextpy.interfaces.page": ["page"], + "nextpy.interfaces.web.components.proxy": ["animation", "unstyled"], + "nextpy.interfaces.web.style": ["color_mode", "style", "toggle_color_mode"], + "nextpy.interfaces.web.components.recharts": [ "area_chart", "bar_chart", "line_chart", "composed_chart", "pie_chart", "radar_chart", "radial_bar_chart", "scatter_chart", "funnel_chart", "treemap", "area", "bar", "line", "scatter", "x_axis", "y_axis", "z_axis", "brush", @@ -300,7 +321,6 @@ "polar_angle_axis", "polar_grid", "polar_radius_axis", ], "nextpy.utils": ["utils"], - "nextpy.frontend.components.proxy": ["animation"], } @@ -351,10 +371,12 @@ def __getattr__(name: str) -> Type: """ # Custom alias handling if name == "animation": - module = importlib.import_module("nextpy.frontend.components.proxy") + module = importlib.import_module("nextpy.interfaces.web.components.proxy") return module.animation - + # Custom alias handling for 'unstyled' + if name == "unstyled": + return importlib.import_module("nextpy.interfaces.web.components.proxy.unstyled") try: # Check for import of a module that is not in the mapping. if name not in _MAPPING: @@ -371,4 +393,4 @@ def __getattr__(name: str) -> Type: getattr(module, name) if name != _MAPPING[name].rsplit(".")[-1] else module ) except ModuleNotFoundError: - raise AttributeError(f"module 'nextpy' has no attribute {name}") from None + raise AttributeError(f"module 'nextpy' has no attribute {name}") from None \ No newline at end of file diff --git a/nextpy/__init__.pyi b/nextpy/__init__.pyi index 1f049903..b6cc8b76 100644 --- a/nextpy/__init__.pyi +++ b/nextpy/__init__.pyi @@ -1,4 +1,4 @@ -# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. +# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. # We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. from nextpy.backend import admin as admin @@ -10,453 +10,483 @@ from nextpy import base as base from nextpy.base import Base as Base from nextpy.build import compiler as compiler from nextpy.build.compiler.utils import get_asset_path as get_asset_path -from nextpy.frontend.components import Accordion as Accordion -from nextpy.frontend.components import AccordionButton as AccordionButton -from nextpy.frontend.components import AccordionIcon as AccordionIcon -from nextpy.frontend.components import AccordionItem as AccordionItem -from nextpy.frontend.components import AccordionPanel as AccordionPanel -from nextpy.frontend.components import Alert as Alert -from nextpy.frontend.components import AlertDescription as AlertDescription -from nextpy.frontend.components import AlertDialog as AlertDialog -from nextpy.frontend.components import AlertDialogBody as AlertDialogBody -from nextpy.frontend.components import AlertDialogContent as AlertDialogContent -from nextpy.frontend.components import AlertDialogFooter as AlertDialogFooter -from nextpy.frontend.components import AlertDialogHeader as AlertDialogHeader -from nextpy.frontend.components import AlertDialogOverlay as AlertDialogOverlay -from nextpy.frontend.components import AlertIcon as AlertIcon -from nextpy.frontend.components import AlertTitle as AlertTitle -from nextpy.frontend.components import AspectRatio as AspectRatio -from nextpy.frontend.components import Audio as Audio -from nextpy.frontend.components import Avatar as Avatar -from nextpy.frontend.components import AvatarBadge as AvatarBadge -from nextpy.frontend.components import AvatarGroup as AvatarGroup -from nextpy.frontend.components import Badge as Badge -from nextpy.frontend.components import Box as Box -from nextpy.frontend.components import Breadcrumb as Breadcrumb -from nextpy.frontend.components import BreadcrumbItem as BreadcrumbItem -from nextpy.frontend.components import BreadcrumbLink as BreadcrumbLink -from nextpy.frontend.components import BreadcrumbSeparator as BreadcrumbSeparator -from nextpy.frontend.components import Button as Button -from nextpy.frontend.components import ButtonGroup as ButtonGroup -from nextpy.frontend.components import Card as Card -from nextpy.frontend.components import CardBody as CardBody -from nextpy.frontend.components import CardFooter as CardFooter -from nextpy.frontend.components import CardHeader as CardHeader -from nextpy.frontend.components import Center as Center -from nextpy.frontend.components import Checkbox as Checkbox -from nextpy.frontend.components import CheckboxGroup as CheckboxGroup -from nextpy.frontend.components import CircularProgress as CircularProgress -from nextpy.frontend.components import CircularProgressLabel as CircularProgressLabel -from nextpy.frontend.components import Circle as Circle -from nextpy.frontend.components import Code as Code -from nextpy.frontend.components import CodeBlock as CodeBlock -from nextpy.frontend.components import Collapse as Collapse -from nextpy.frontend.components import ColorModeButton as ColorModeButton -from nextpy.frontend.components import ColorModeIcon as ColorModeIcon -from nextpy.frontend.components import ColorModeSwitch as ColorModeSwitch -from nextpy.frontend.components import Component as Component -from nextpy.frontend.components import Cond as Cond -from nextpy.frontend.components import ConnectionBanner as ConnectionBanner -from nextpy.frontend.components import ConnectionModal as ConnectionModal -from nextpy.frontend.components import Container as Container -from nextpy.frontend.components import DataTable as DataTable -from nextpy.frontend.components import DataEditor as DataEditor -from nextpy.frontend.components import DataEditorTheme as DataEditorTheme -from nextpy.frontend.components import DatePicker as DatePicker -from nextpy.frontend.components import DateTimePicker as DateTimePicker -from nextpy.frontend.components import DebounceInput as DebounceInput -from nextpy.frontend.components import Divider as Divider -from nextpy.frontend.components import Drawer as Drawer -from nextpy.frontend.components import DrawerBody as DrawerBody -from nextpy.frontend.components import DrawerCloseButton as DrawerCloseButton -from nextpy.frontend.components import DrawerContent as DrawerContent -from nextpy.frontend.components import DrawerFooter as DrawerFooter -from nextpy.frontend.components import DrawerHeader as DrawerHeader -from nextpy.frontend.components import DrawerOverlay as DrawerOverlay -from nextpy.frontend.components import Editable as Editable -from nextpy.frontend.components import EditableInput as EditableInput -from nextpy.frontend.components import EditablePreview as EditablePreview -from nextpy.frontend.components import EditableTextarea as EditableTextarea -from nextpy.frontend.components import Editor as Editor -from nextpy.frontend.components import Email as Email -from nextpy.frontend.components import Fade as Fade -from nextpy.frontend.components import Flex as Flex -from nextpy.frontend.components import Foreach as Foreach -from nextpy.frontend.components import Form as Form -from nextpy.frontend.components import FormControl as FormControl -from nextpy.frontend.components import FormErrorMessage as FormErrorMessage -from nextpy.frontend.components import FormHelperText as FormHelperText -from nextpy.frontend.components import FormLabel as FormLabel -from nextpy.frontend.components import Fragment as Fragment -from nextpy.frontend.components import Grid as Grid -from nextpy.frontend.components import GridItem as GridItem -from nextpy.frontend.components import Heading as Heading -from nextpy.frontend.components import Highlight as Highlight -from nextpy.frontend.components import Hstack as Hstack -from nextpy.frontend.components import Html as Html -from nextpy.frontend.components import Icon as Icon -from nextpy.frontend.components import IconButton as IconButton -from nextpy.frontend.components import Image as Image -from nextpy.frontend.components import Input as Input -from nextpy.frontend.components import InputGroup as InputGroup -from nextpy.frontend.components import InputLeftAddon as InputLeftAddon -from nextpy.frontend.components import InputLeftElement as InputLeftElement -from nextpy.frontend.components import InputRightAddon as InputRightAddon -from nextpy.frontend.components import InputRightElement as InputRightElement -from nextpy.frontend.components import Kbd as Kbd -from nextpy.frontend.components import Link as Link -from nextpy.frontend.components import LinkBox as LinkBox -from nextpy.frontend.components import LinkOverlay as LinkOverlay -from nextpy.frontend.components import List as List -from nextpy.frontend.components import ListItem as ListItem -from nextpy.frontend.components import Markdown as Markdown -from nextpy.frontend.components import Match as Match -from nextpy.frontend.components import Menu as Menu -from nextpy.frontend.components import MenuButton as MenuButton -from nextpy.frontend.components import MenuDivider as MenuDivider -from nextpy.frontend.components import MenuGroup as MenuGroup -from nextpy.frontend.components import MenuItem as MenuItem -from nextpy.frontend.components import MenuItemOption as MenuItemOption -from nextpy.frontend.components import MenuList as MenuList -from nextpy.frontend.components import MenuOptionGroup as MenuOptionGroup -from nextpy.frontend.components import Modal as Modal -from nextpy.frontend.components import ModalBody as ModalBody -from nextpy.frontend.components import ModalCloseButton as ModalCloseButton -from nextpy.frontend.components import ModalContent as ModalContent -from nextpy.frontend.components import ModalFooter as ModalFooter -from nextpy.frontend.components import ModalHeader as ModalHeader -from nextpy.frontend.components import ModalOverlay as ModalOverlay -from nextpy.frontend.components import Moment as Moment -from nextpy.frontend.components import MultiSelect as MultiSelect -from nextpy.frontend.components import MultiSelectOption as MultiSelectOption -from nextpy.frontend.components import NextLink as NextLink -from nextpy.frontend.components import NumberDecrementStepper as NumberDecrementStepper -from nextpy.frontend.components import NumberIncrementStepper as NumberIncrementStepper -from nextpy.frontend.components import NumberInput as NumberInput -from nextpy.frontend.components import NumberInputField as NumberInputField -from nextpy.frontend.components import NumberInputStepper as NumberInputStepper -from nextpy.frontend.components import Option as Option -from nextpy.frontend.components import OrderedList as OrderedList -from nextpy.frontend.components import Password as Password -from nextpy.frontend.components import PinInput as PinInput -from nextpy.frontend.components import PinInputField as PinInputField -from nextpy.frontend.components import Plotly as Plotly -from nextpy.frontend.components import Popover as Popover -from nextpy.frontend.components import PopoverAnchor as PopoverAnchor -from nextpy.frontend.components import PopoverArrow as PopoverArrow -from nextpy.frontend.components import PopoverBody as PopoverBody -from nextpy.frontend.components import PopoverCloseButton as PopoverCloseButton -from nextpy.frontend.components import PopoverContent as PopoverContent -from nextpy.frontend.components import PopoverFooter as PopoverFooter -from nextpy.frontend.components import PopoverHeader as PopoverHeader -from nextpy.frontend.components import PopoverTrigger as PopoverTrigger -from nextpy.frontend.components import Progress as Progress -from nextpy.frontend.components import Radio as Radio -from nextpy.frontend.components import RadioGroup as RadioGroup -from nextpy.frontend.components import RangeSlider as RangeSlider -from nextpy.frontend.components import RangeSliderFilledTrack as RangeSliderFilledTrack -from nextpy.frontend.components import RangeSliderThumb as RangeSliderThumb -from nextpy.frontend.components import RangeSliderTrack as RangeSliderTrack -from nextpy.frontend.components import ResponsiveGrid as ResponsiveGrid -from nextpy.frontend.components import ScaleFade as ScaleFade -from nextpy.frontend.components import Script as Script -from nextpy.frontend.components import Select as Select -from nextpy.frontend.components import Skeleton as Skeleton -from nextpy.frontend.components import SkeletonCircle as SkeletonCircle -from nextpy.frontend.components import SkeletonText as SkeletonText -from nextpy.frontend.components import Slide as Slide -from nextpy.frontend.components import SlideFade as SlideFade -from nextpy.frontend.components import Slider as Slider -from nextpy.frontend.components import SliderFilledTrack as SliderFilledTrack -from nextpy.frontend.components import SliderMark as SliderMark -from nextpy.frontend.components import SliderThumb as SliderThumb -from nextpy.frontend.components import SliderTrack as SliderTrack -from nextpy.frontend.components import Spacer as Spacer -from nextpy.frontend.components import Span as Span -from nextpy.frontend.components import Spinner as Spinner -from nextpy.frontend.components import Square as Square -from nextpy.frontend.components import Stack as Stack -from nextpy.frontend.components import Stat as Stat -from nextpy.frontend.components import StatArrow as StatArrow -from nextpy.frontend.components import StatGroup as StatGroup -from nextpy.frontend.components import StatHelpText as StatHelpText -from nextpy.frontend.components import StatLabel as StatLabel -from nextpy.frontend.components import StatNumber as StatNumber -from nextpy.frontend.components import Step as Step -from nextpy.frontend.components import StepDescription as StepDescription -from nextpy.frontend.components import StepIcon as StepIcon -from nextpy.frontend.components import StepIndicator as StepIndicator -from nextpy.frontend.components import StepNumber as StepNumber -from nextpy.frontend.components import StepSeparator as StepSeparator -from nextpy.frontend.components import StepStatus as StepStatus -from nextpy.frontend.components import StepTitle as StepTitle -from nextpy.frontend.components import Stepper as Stepper -from nextpy.frontend.components import Switch as Switch -from nextpy.frontend.components import Tab as Tab -from nextpy.frontend.components import TabList as TabList -from nextpy.frontend.components import TabPanel as TabPanel -from nextpy.frontend.components import TabPanels as TabPanels -from nextpy.frontend.components import Table as Table -from nextpy.frontend.components import TableCaption as TableCaption -from nextpy.frontend.components import TableContainer as TableContainer -from nextpy.frontend.components import Tabs as Tabs -from nextpy.frontend.components import Tag as Tag -from nextpy.frontend.components import TagCloseButton as TagCloseButton -from nextpy.frontend.components import TagLabel as TagLabel -from nextpy.frontend.components import TagLeftIcon as TagLeftIcon -from nextpy.frontend.components import TagRightIcon as TagRightIcon -from nextpy.frontend.components import Tbody as Tbody -from nextpy.frontend.components import Td as Td -from nextpy.frontend.components import Text as Text -from nextpy.frontend.components import TextArea as TextArea -from nextpy.frontend.components import Tfoot as Tfoot -from nextpy.frontend.components import Th as Th -from nextpy.frontend.components import Thead as Thead -from nextpy.frontend.components import Tooltip as Tooltip -from nextpy.frontend.components import Tr as Tr -from nextpy.frontend.components import UnorderedList as UnorderedList -from nextpy.frontend.components import Upload as Upload -from nextpy.frontend.components import Video as Video -from nextpy.frontend.components import VisuallyHidden as VisuallyHidden -from nextpy.frontend.components import Vstack as Vstack -from nextpy.frontend.components import Wrap as Wrap -from nextpy.frontend.components import WrapItem as WrapItem -from nextpy.frontend.components import accordion as accordion -from nextpy.frontend.components import accordion_button as accordion_button -from nextpy.frontend.components import accordion_icon as accordion_icon -from nextpy.frontend.components import accordion_item as accordion_item -from nextpy.frontend.components import accordion_panel as accordion_panel -from nextpy.frontend.components import alert as alert -from nextpy.frontend.components import alert_description as alert_description -from nextpy.frontend.components import alert_dialog as alert_dialog -from nextpy.frontend.components import alert_dialog_body as alert_dialog_body -from nextpy.frontend.components import alert_dialog_content as alert_dialog_content -from nextpy.frontend.components import alert_dialog_footer as alert_dialog_footer -from nextpy.frontend.components import alert_dialog_header as alert_dialog_header -from nextpy.frontend.components import alert_dialog_overlay as alert_dialog_overlay -from nextpy.frontend.components import alert_icon as alert_icon -from nextpy.frontend.components import alert_title as alert_title -from nextpy.frontend.components import aspect_ratio as aspect_ratio -from nextpy.frontend.components import audio as audio -from nextpy.frontend.components import avatar as avatar -from nextpy.frontend.components import avatar_badge as avatar_badge -from nextpy.frontend.components import avatar_group as avatar_group -from nextpy.frontend.components import badge as badge -from nextpy.frontend.components import box as box -from nextpy.frontend.components import breadcrumb as breadcrumb -from nextpy.frontend.components import breadcrumb_item as breadcrumb_item -from nextpy.frontend.components import breadcrumb_link as breadcrumb_link -from nextpy.frontend.components import breadcrumb_separator as breadcrumb_separator -from nextpy.frontend.components import button as button -from nextpy.frontend.components import button_group as button_group -from nextpy.frontend.components import card as card -from nextpy.frontend.components import card_body as card_body -from nextpy.frontend.components import card_footer as card_footer -from nextpy.frontend.components import card_header as card_header -from nextpy.frontend.components import center as center -from nextpy.frontend.components import checkbox as checkbox -from nextpy.frontend.components import checkbox_group as checkbox_group -from nextpy.frontend.components import circular_progress as circular_progress -from nextpy.frontend.components import circular_progress_label as circular_progress_label -from nextpy.frontend.components import circle as circle -from nextpy.frontend.components import code as code -from nextpy.frontend.components import code_block as code_block -from nextpy.frontend.components import collapse as collapse -from nextpy.frontend.components import color_mode_button as color_mode_button -from nextpy.frontend.components import color_mode_icon as color_mode_icon -from nextpy.frontend.components import color_mode_switch as color_mode_switch -from nextpy.frontend.components import component as component -from nextpy.frontend.components import cond as cond -from nextpy.frontend.components import connection_banner as connection_banner -from nextpy.frontend.components import connection_modal as connection_modal -from nextpy.frontend.components import container as container -from nextpy.frontend.components import data_table as data_table -from nextpy.frontend.components import data_editor as data_editor -from nextpy.frontend.components import data_editor_theme as data_editor_theme -from nextpy.frontend.components import date_picker as date_picker -from nextpy.frontend.components import date_time_picker as date_time_picker -from nextpy.frontend.components import debounce_input as debounce_input -from nextpy.frontend.components import divider as divider -from nextpy.frontend.components import drawer as drawer -from nextpy.frontend.components import drawer_body as drawer_body -from nextpy.frontend.components import drawer_close_button as drawer_close_button -from nextpy.frontend.components import drawer_content as drawer_content -from nextpy.frontend.components import drawer_footer as drawer_footer -from nextpy.frontend.components import drawer_header as drawer_header -from nextpy.frontend.components import drawer_overlay as drawer_overlay -from nextpy.frontend.components import editable as editable -from nextpy.frontend.components import editable_input as editable_input -from nextpy.frontend.components import editable_preview as editable_preview -from nextpy.frontend.components import editable_textarea as editable_textarea -from nextpy.frontend.components import editor as editor -from nextpy.frontend.components import email as email -from nextpy.frontend.components import fade as fade -from nextpy.frontend.components import flex as flex -from nextpy.frontend.components import foreach as foreach -from nextpy.frontend.components import form as form -from nextpy.frontend.components import form_control as form_control -from nextpy.frontend.components import form_error_message as form_error_message -from nextpy.frontend.components import form_helper_text as form_helper_text -from nextpy.frontend.components import form_label as form_label -from nextpy.frontend.components import fragment as fragment -from nextpy.frontend.components import grid as grid -from nextpy.frontend.components import grid_item as grid_item -from nextpy.frontend.components import heading as heading -from nextpy.frontend.components import highlight as highlight -from nextpy.frontend.components import hstack as hstack -from nextpy.frontend.components import html as html -from nextpy.frontend.components import icon as icon -from nextpy.frontend.components import icon_button as icon_button -from nextpy.frontend.components import image as image -from nextpy.frontend.components import input as input -from nextpy.frontend.components import input_group as input_group -from nextpy.frontend.components import input_left_addon as input_left_addon -from nextpy.frontend.components import input_left_element as input_left_element -from nextpy.frontend.components import input_right_addon as input_right_addon -from nextpy.frontend.components import input_right_element as input_right_element -from nextpy.frontend.components import kbd as kbd -from nextpy.frontend.components import link as link -from nextpy.frontend.components import link_box as link_box -from nextpy.frontend.components import link_overlay as link_overlay -from nextpy.frontend.components import list as list -from nextpy.frontend.components import list_item as list_item -from nextpy.frontend.components import markdown as markdown -from nextpy.frontend.components import match as match -from nextpy.frontend.components import menu as menu -from nextpy.frontend.components import menu_button as menu_button -from nextpy.frontend.components import menu_divider as menu_divider -from nextpy.frontend.components import menu_group as menu_group -from nextpy.frontend.components import menu_item as menu_item -from nextpy.frontend.components import menu_item_option as menu_item_option -from nextpy.frontend.components import menu_list as menu_list -from nextpy.frontend.components import menu_option_group as menu_option_group -from nextpy.frontend.components import modal as modal -from nextpy.frontend.components import modal_body as modal_body -from nextpy.frontend.components import modal_close_button as modal_close_button -from nextpy.frontend.components import modal_content as modal_content -from nextpy.frontend.components import modal_footer as modal_footer -from nextpy.frontend.components import modal_header as modal_header -from nextpy.frontend.components import modal_overlay as modal_overlay -from nextpy.frontend.components import moment as moment -from nextpy.frontend.components import multi_select as multi_select -from nextpy.frontend.components import multi_select_option as multi_select_option -from nextpy.frontend.components import next_link as next_link -from nextpy.frontend.components import number_decrement_stepper as number_decrement_stepper -from nextpy.frontend.components import number_increment_stepper as number_increment_stepper -from nextpy.frontend.components import number_input as number_input -from nextpy.frontend.components import number_input_field as number_input_field -from nextpy.frontend.components import number_input_stepper as number_input_stepper -from nextpy.frontend.components import option as option -from nextpy.frontend.components import ordered_list as ordered_list -from nextpy.frontend.components import password as password -from nextpy.frontend.components import pin_input as pin_input -from nextpy.frontend.components import pin_input_field as pin_input_field -from nextpy.frontend.components import plotly as plotly -from nextpy.frontend.components import popover as popover -from nextpy.frontend.components import popover_anchor as popover_anchor -from nextpy.frontend.components import popover_arrow as popover_arrow -from nextpy.frontend.components import popover_body as popover_body -from nextpy.frontend.components import popover_close_button as popover_close_button -from nextpy.frontend.components import popover_content as popover_content -from nextpy.frontend.components import popover_footer as popover_footer -from nextpy.frontend.components import popover_header as popover_header -from nextpy.frontend.components import popover_trigger as popover_trigger -from nextpy.frontend.components import progress as progress -from nextpy.frontend.components import radio as radio -from nextpy.frontend.components import radio_group as radio_group -from nextpy.frontend.components import range_slider as range_slider -from nextpy.frontend.components import range_slider_filled_track as range_slider_filled_track -from nextpy.frontend.components import range_slider_thumb as range_slider_thumb -from nextpy.frontend.components import range_slider_track as range_slider_track -from nextpy.frontend.components import responsive_grid as responsive_grid -from nextpy.frontend.components import scale_fade as scale_fade -from nextpy.frontend.components import script as script -from nextpy.frontend.components import select as select -from nextpy.frontend.components import skeleton as skeleton -from nextpy.frontend.components import skeleton_circle as skeleton_circle -from nextpy.frontend.components import skeleton_text as skeleton_text -from nextpy.frontend.components import slide as slide -from nextpy.frontend.components import slide_fade as slide_fade -from nextpy.frontend.components import slider as slider -from nextpy.frontend.components import slider_filled_track as slider_filled_track -from nextpy.frontend.components import slider_mark as slider_mark -from nextpy.frontend.components import slider_thumb as slider_thumb -from nextpy.frontend.components import slider_track as slider_track -from nextpy.frontend.components import spacer as spacer -from nextpy.frontend.components import span as span -from nextpy.frontend.components import spinner as spinner -from nextpy.frontend.components import square as square -from nextpy.frontend.components import stack as stack -from nextpy.frontend.components import stat as stat -from nextpy.frontend.components import stat_arrow as stat_arrow -from nextpy.frontend.components import stat_group as stat_group -from nextpy.frontend.components import stat_help_text as stat_help_text -from nextpy.frontend.components import stat_label as stat_label -from nextpy.frontend.components import stat_number as stat_number -from nextpy.frontend.components import step as step -from nextpy.frontend.components import step_description as step_description -from nextpy.frontend.components import step_icon as step_icon -from nextpy.frontend.components import step_indicator as step_indicator -from nextpy.frontend.components import step_number as step_number -from nextpy.frontend.components import step_separator as step_separator -from nextpy.frontend.components import step_status as step_status -from nextpy.frontend.components import step_title as step_title -from nextpy.frontend.components import stepper as stepper -from nextpy.frontend.components import switch as switch -from nextpy.frontend.components import tab as tab -from nextpy.frontend.components import tab_list as tab_list -from nextpy.frontend.components import tab_panel as tab_panel -from nextpy.frontend.components import tab_panels as tab_panels -from nextpy.frontend.components import table as table -from nextpy.frontend.components import table_caption as table_caption -from nextpy.frontend.components import table_container as table_container -from nextpy.frontend.components import tabs as tabs -from nextpy.frontend.components import tag as tag -from nextpy.frontend.components import tag_close_button as tag_close_button -from nextpy.frontend.components import tag_label as tag_label -from nextpy.frontend.components import tag_left_icon as tag_left_icon -from nextpy.frontend.components import tag_right_icon as tag_right_icon -from nextpy.frontend.components import tbody as tbody -from nextpy.frontend.components import td as td -from nextpy.frontend.components import text as text -from nextpy.frontend.components import text_area as text_area -from nextpy.frontend.components import tfoot as tfoot -from nextpy.frontend.components import th as th -from nextpy.frontend.components import thead as thead -from nextpy.frontend.components import tooltip as tooltip -from nextpy.frontend.components import tr as tr -from nextpy.frontend.components import unordered_list as unordered_list -from nextpy.frontend.components import upload as upload -from nextpy.frontend.components import video as video -from nextpy.frontend.components import visually_hidden as visually_hidden -from nextpy.frontend.components import vstack as vstack -from nextpy.frontend.components import wrap as wrap -from nextpy.frontend.components import wrap_item as wrap_item -from nextpy.frontend.components import cancel_upload as cancel_upload -from nextpy.frontend import components as components -from nextpy.frontend.components import color_mode_cond as color_mode_cond -from nextpy.frontend.components import desktop_only as desktop_only -from nextpy.frontend.components import mobile_only as mobile_only -from nextpy.frontend.components import tablet_only as tablet_only -from nextpy.frontend.components import mobile_and_tablet as mobile_and_tablet -from nextpy.frontend.components import tablet_and_desktop as tablet_and_desktop -from nextpy.frontend.components import selected_files as selected_files -from nextpy.frontend.components import clear_selected_files as clear_selected_files -from nextpy.frontend.components import EditorButtonList as EditorButtonList -from nextpy.frontend.components import EditorOptions as EditorOptions -from nextpy.frontend.components import NoSSRComponent as NoSSRComponent -from nextpy.frontend.components import chakra as chakra -from nextpy.frontend.components import next as next -from nextpy.frontend.components.component import memo as memo -from nextpy.frontend.components import recharts as recharts -from nextpy.frontend.components.moment.moment import MomentDelta as MomentDelta +from nextpy.interfaces.web.components import Accordion as Accordion +from nextpy.interfaces.web.components import AccordionButton as AccordionButton +from nextpy.interfaces.web.components import AccordionIcon as AccordionIcon +from nextpy.interfaces.web.components import AccordionItem as AccordionItem +from nextpy.interfaces.web.components import AccordionPanel as AccordionPanel +from nextpy.interfaces.web.components import Alert as Alert +from nextpy.interfaces.web.components import AlertDescription as AlertDescription +from nextpy.interfaces.web.components import AlertDialog as AlertDialog +from nextpy.interfaces.web.components import AlertDialogBody as AlertDialogBody +from nextpy.interfaces.web.components import AlertDialogContent as AlertDialogContent +from nextpy.interfaces.web.components import AlertDialogFooter as AlertDialogFooter +from nextpy.interfaces.web.components import AlertDialogHeader as AlertDialogHeader +from nextpy.interfaces.web.components import AlertDialogOverlay as AlertDialogOverlay +from nextpy.interfaces.web.components import AlertIcon as AlertIcon +from nextpy.interfaces.web.components import AlertTitle as AlertTitle +from nextpy.interfaces.web.components import AspectRatio as AspectRatio +from nextpy.interfaces.web.components import Audio as Audio +from nextpy.interfaces.web.components import Avatar as Avatar +from nextpy.interfaces.web.components import AvatarBadge as AvatarBadge +from nextpy.interfaces.web.components import AvatarGroup as AvatarGroup +from nextpy.interfaces.web.components import Badge as Badge +from nextpy.interfaces.web.components import Box as Box +from nextpy.interfaces.web.components import Breadcrumb as Breadcrumb +from nextpy.interfaces.web.components import BreadcrumbItem as BreadcrumbItem +from nextpy.interfaces.web.components import BreadcrumbLink as BreadcrumbLink +from nextpy.interfaces.web.components import BreadcrumbSeparator as BreadcrumbSeparator +from nextpy.interfaces.web.components import Button as Button +from nextpy.interfaces.web.components import ButtonGroup as ButtonGroup +from nextpy.interfaces.web.components import Card as Card +from nextpy.interfaces.web.components import CardBody as CardBody +from nextpy.interfaces.web.components import CardFooter as CardFooter +from nextpy.interfaces.web.components import CardHeader as CardHeader +from nextpy.interfaces.web.components import Center as Center +from nextpy.interfaces.web.components import Checkbox as Checkbox +from nextpy.interfaces.web.components import CheckboxGroup as CheckboxGroup +from nextpy.interfaces.web.components import CircularProgress as CircularProgress +from nextpy.interfaces.web.components import CircularProgressLabel as CircularProgressLabel +from nextpy.interfaces.web.components import Circle as Circle +from nextpy.interfaces.web.components import Code as Code +from nextpy.interfaces.web.components import CodeBlock as CodeBlock +from nextpy.interfaces.web.components import Collapse as Collapse +from nextpy.interfaces.web.components import ColorModeButton as ColorModeButton +from nextpy.interfaces.web.components import ColorModeIcon as ColorModeIcon +from nextpy.interfaces.web.components import ColorModeSwitch as ColorModeSwitch +from nextpy.interfaces.web.components import ColorPicker as ColorPicker +from nextpy.interfaces.web.components import Component as Component +from nextpy.interfaces.web.components import Cond as Cond +from nextpy.interfaces.web.components import ConnectionBanner as ConnectionBanner +from nextpy.interfaces.web.components import ConnectionModal as ConnectionModal +from nextpy.interfaces.web.components import Container as Container +from nextpy.interfaces.web.components import DataTable as DataTable +from nextpy.interfaces.web.components import DataEditor as DataEditor +from nextpy.interfaces.web.components import DataFrame as DataFrame +from nextpy.interfaces.web.components import DataEditorTheme as DataEditorTheme +from nextpy.interfaces.web.components import DatePicker as DatePicker +from nextpy.interfaces.web.components import DateTimePicker as DateTimePicker +from nextpy.interfaces.web.components import DebounceInput as DebounceInput +from nextpy.interfaces.web.components import Divider as Divider +from nextpy.interfaces.web.components import Drawer as Drawer +from nextpy.interfaces.web.components import DrawerBody as DrawerBody +from nextpy.interfaces.web.components import DrawerCloseButton as DrawerCloseButton +from nextpy.interfaces.web.components import DrawerContent as DrawerContent +from nextpy.interfaces.web.components import DrawerFooter as DrawerFooter +from nextpy.interfaces.web.components import DrawerHeader as DrawerHeader +from nextpy.interfaces.web.components import DrawerOverlay as DrawerOverlay +from nextpy.interfaces.web.components import Editable as Editable +from nextpy.interfaces.web.components import EditableInput as EditableInput +from nextpy.interfaces.web.components import EditablePreview as EditablePreview +from nextpy.interfaces.web.components import EditableTextarea as EditableTextarea +from nextpy.interfaces.web.components import Editor as Editor +from nextpy.interfaces.web.components import Email as Email +from nextpy.interfaces.web.components import Expander as Expander +from nextpy.interfaces.web.components import ExpanderButton as ExpanderButton +from nextpy.interfaces.web.components import ExpanderIcon as ExpanderIcon +from nextpy.interfaces.web.components import ExpanderItem as ExpanderItem +from nextpy.interfaces.web.components import ExpanderPanel as ExpanderPanel +from nextpy.interfaces.web.components import Fade as Fade +from nextpy.interfaces.web.components import Flex as Flex +from nextpy.interfaces.web.components import Foreach as Foreach +from nextpy.interfaces.web.components import Form as Form +from nextpy.interfaces.web.components import FormControl as FormControl +from nextpy.interfaces.web.components import FormErrorMessage as FormErrorMessage +from nextpy.interfaces.web.components import FormHelperText as FormHelperText +from nextpy.interfaces.web.components import FormLabel as FormLabel +from nextpy.interfaces.web.components import Fragment as Fragment +from nextpy.interfaces.web.components import Grid as Grid +from nextpy.interfaces.web.components import GridItem as GridItem +from nextpy.interfaces.web.components import Heading as Heading +from nextpy.interfaces.web.components import Highlight as Highlight +from nextpy.interfaces.web.components import Hstack as Hstack +from nextpy.interfaces.web.components import Html as Html +from nextpy.interfaces.web.components import Icon as Icon +from nextpy.interfaces.web.components import IconButton as IconButton +from nextpy.interfaces.web.components import Image as Image +from nextpy.interfaces.web.components import Input as Input +from nextpy.interfaces.web.components import InputGroup as InputGroup +from nextpy.interfaces.web.components import InputLeftAddon as InputLeftAddon +from nextpy.interfaces.web.components import InputLeftElement as InputLeftElement +from nextpy.interfaces.web.components import InputRightAddon as InputRightAddon +from nextpy.interfaces.web.components import InputRightElement as InputRightElement +from nextpy.interfaces.web.components import Kbd as Kbd +from nextpy.interfaces.web.components import Link as Link +from nextpy.interfaces.web.components import LinkBox as LinkBox +from nextpy.interfaces.web.components import LinkOverlay as LinkOverlay +from nextpy.interfaces.web.components import List as List +from nextpy.interfaces.web.components import ListItem as ListItem +from nextpy.interfaces.web.components import Markdown as Markdown +from nextpy.interfaces.web.components import Match as Match +from nextpy.interfaces.web.components import Menu as Menu +from nextpy.interfaces.web.components import MenuButton as MenuButton +from nextpy.interfaces.web.components import MenuDivider as MenuDivider +from nextpy.interfaces.web.components import MenuGroup as MenuGroup +from nextpy.interfaces.web.components import MenuItem as MenuItem +from nextpy.interfaces.web.components import MenuItemOption as MenuItemOption +from nextpy.interfaces.web.components import MenuList as MenuList +from nextpy.interfaces.web.components import MenuOptionGroup as MenuOptionGroup +from nextpy.interfaces.web.components import Modal as Modal +from nextpy.interfaces.web.components import ModalBody as ModalBody +from nextpy.interfaces.web.components import ModalCloseButton as ModalCloseButton +from nextpy.interfaces.web.components import ModalContent as ModalContent +from nextpy.interfaces.web.components import ModalFooter as ModalFooter +from nextpy.interfaces.web.components import ModalHeader as ModalHeader +from nextpy.interfaces.web.components import ModalOverlay as ModalOverlay +from nextpy.interfaces.web.components import Moment as Moment +from nextpy.interfaces.web.components import MultiSelect as MultiSelect +from nextpy.interfaces.web.components import MultiSelectOption as MultiSelectOption +from nextpy.interfaces.web.components import NextLink as NextLink +from nextpy.interfaces.web.components import NumberDecrementStepper as NumberDecrementStepper +from nextpy.interfaces.web.components import NumberIncrementStepper as NumberIncrementStepper +from nextpy.interfaces.web.components import NumberInput as NumberInput +from nextpy.interfaces.web.components import NumberInputField as NumberInputField +from nextpy.interfaces.web.components import NumberInputStepper as NumberInputStepper +from nextpy.interfaces.web.components import Option as Option +from nextpy.interfaces.web.components import OrderedList as OrderedList +from nextpy.interfaces.web.components import Password as Password +from nextpy.interfaces.web.components import PinInput as PinInput +from nextpy.interfaces.web.components import PinInputField as PinInputField +from nextpy.interfaces.web.components import Plotly as Plotly +from nextpy.interfaces.web.components import Popover as Popover +from nextpy.interfaces.web.components import PopoverAnchor as PopoverAnchor +from nextpy.interfaces.web.components import PopoverArrow as PopoverArrow +from nextpy.interfaces.web.components import PopoverBody as PopoverBody +from nextpy.interfaces.web.components import PopoverCloseButton as PopoverCloseButton +from nextpy.interfaces.web.components import PopoverContent as PopoverContent +from nextpy.interfaces.web.components import PopoverFooter as PopoverFooter +from nextpy.interfaces.web.components import PopoverHeader as PopoverHeader +from nextpy.interfaces.web.components import PopoverTrigger as PopoverTrigger +from nextpy.interfaces.web.components import Progress as Progress +from nextpy.interfaces.web.components import Radio as Radio +from nextpy.interfaces.web.components import RadioGroup as RadioGroup +from nextpy.interfaces.web.components import RangeSlider as RangeSlider +from nextpy.interfaces.web.components import RangeSliderFilledTrack as RangeSliderFilledTrack +from nextpy.interfaces.web.components import RangeSliderThumb as RangeSliderThumb +from nextpy.interfaces.web.components import RangeSliderTrack as RangeSliderTrack +from nextpy.interfaces.web.components import ResponsiveGrid as ResponsiveGrid +from nextpy.interfaces.web.components import ScaleFade as ScaleFade +from nextpy.interfaces.web.components import Script as Script +from nextpy.interfaces.web.components import Select as Select +from nextpy.interfaces.web.components import Skeleton as Skeleton +from nextpy.interfaces.web.components import SkeletonCircle as SkeletonCircle +from nextpy.interfaces.web.components import SkeletonText as SkeletonText +from nextpy.interfaces.web.components import Slide as Slide +from nextpy.interfaces.web.components import SlideFade as SlideFade +from nextpy.interfaces.web.components import Slider as Slider +from nextpy.interfaces.web.components import SliderFilledTrack as SliderFilledTrack +from nextpy.interfaces.web.components import SliderMark as SliderMark +from nextpy.interfaces.web.components import SliderThumb as SliderThumb +from nextpy.interfaces.web.components import SliderTrack as SliderTrack +from nextpy.interfaces.web.components import Spacer as Spacer +from nextpy.interfaces.web.components import Span as Span +from nextpy.interfaces.web.components import Spinner as Spinner +from nextpy.interfaces.web.components import Square as Square +from nextpy.interfaces.web.components import Stack as Stack +from nextpy.interfaces.web.components import Stat as Stat +from nextpy.interfaces.web.components import StatArrow as StatArrow +from nextpy.interfaces.web.components import StatGroup as StatGroup +from nextpy.interfaces.web.components import StatHelpText as StatHelpText +from nextpy.interfaces.web.components import StatLabel as StatLabel +from nextpy.interfaces.web.components import StatNumber as StatNumber +from nextpy.interfaces.web.components import Step as Step +from nextpy.interfaces.web.components import StepDescription as StepDescription +from nextpy.interfaces.web.components import StepIcon as StepIcon +from nextpy.interfaces.web.components import StepIndicator as StepIndicator +from nextpy.interfaces.web.components import StepNumber as StepNumber +from nextpy.interfaces.web.components import StepSeparator as StepSeparator +from nextpy.interfaces.web.components import StepStatus as StepStatus +from nextpy.interfaces.web.components import StepTitle as StepTitle +from nextpy.interfaces.web.components import Stepper as Stepper +from nextpy.interfaces.web.components import Switch as Switch +from nextpy.interfaces.web.components import Tab as Tab +from nextpy.interfaces.web.components import TabList as TabList +from nextpy.interfaces.web.components import TabPanel as TabPanel +from nextpy.interfaces.web.components import TabPanels as TabPanels +from nextpy.interfaces.web.components import Table as Table +from nextpy.interfaces.web.components import TableCaption as TableCaption +from nextpy.interfaces.web.components import TableContainer as TableContainer +from nextpy.interfaces.web.components import Tabs as Tabs +from nextpy.interfaces.web.components import Tag as Tag +from nextpy.interfaces.web.components import TagCloseButton as TagCloseButton +from nextpy.interfaces.web.components import TagLabel as TagLabel +from nextpy.interfaces.web.components import TagLeftIcon as TagLeftIcon +from nextpy.interfaces.web.components import TagRightIcon as TagRightIcon +from nextpy.interfaces.web.components import Tbody as Tbody +from nextpy.interfaces.web.components import Td as Td +from nextpy.interfaces.web.components import Text as Text +from nextpy.interfaces.web.components import TextArea as TextArea +from nextpy.interfaces.web.components import Tfoot as Tfoot +from nextpy.interfaces.web.components import Th as Th +from nextpy.interfaces.web.components import Thead as Thead +from nextpy.interfaces.web.components import Tooltip as Tooltip +from nextpy.interfaces.web.components import Tr as Tr +from nextpy.interfaces.web.components import UnorderedList as UnorderedList +from nextpy.interfaces.web.components import Upload as Upload +from nextpy.interfaces.web.components import Video as Video +from nextpy.interfaces.web.components import VisuallyHidden as VisuallyHidden +from nextpy.interfaces.web.components import Vstack as Vstack +from nextpy.interfaces.web.components import Wrap as Wrap +from nextpy.interfaces.web.components import WrapItem as WrapItem +from nextpy.interfaces.web.components import accordion as accordion +from nextpy.interfaces.web.components import accordion_button as accordion_button +from nextpy.interfaces.web.components import accordion_icon as accordion_icon +from nextpy.interfaces.web.components import accordion_item as accordion_item +from nextpy.interfaces.web.components import accordion_panel as accordion_panel +from nextpy.interfaces.web.components import alert as alert +from nextpy.interfaces.web.components import alert_description as alert_description +from nextpy.interfaces.web.components import alert_dialog as alert_dialog +from nextpy.interfaces.web.components import alert_dialog_body as alert_dialog_body +from nextpy.interfaces.web.components import alert_dialog_content as alert_dialog_content +from nextpy.interfaces.web.components import alert_dialog_footer as alert_dialog_footer +from nextpy.interfaces.web.components import alert_dialog_header as alert_dialog_header +from nextpy.interfaces.web.components import alert_dialog_overlay as alert_dialog_overlay +from nextpy.interfaces.web.components import alert_icon as alert_icon +from nextpy.interfaces.web.components import alert_title as alert_title +from nextpy.interfaces.web.components import aspect_ratio as aspect_ratio +from nextpy.interfaces.web.components import audio as audio +from nextpy.interfaces.web.components import avatar as avatar +from nextpy.interfaces.web.components import avatar_badge as avatar_badge +from nextpy.interfaces.web.components import avatar_group as avatar_group +from nextpy.interfaces.web.components import badge as badge +from nextpy.interfaces.web.components import box as box +from nextpy.interfaces.web.components import breadcrumb as breadcrumb +from nextpy.interfaces.web.components import breadcrumb_item as breadcrumb_item +from nextpy.interfaces.web.components import breadcrumb_link as breadcrumb_link +from nextpy.interfaces.web.components import breadcrumb_separator as breadcrumb_separator +from nextpy.interfaces.web.components import button as button +from nextpy.interfaces.web.components import button_group as button_group +from nextpy.interfaces.web.components import card as card +from nextpy.interfaces.web.components import card_body as card_body +from nextpy.interfaces.web.components import card_footer as card_footer +from nextpy.interfaces.web.components import card_header as card_header +from nextpy.interfaces.web.components import center as center +from nextpy.interfaces.web.components import checkbox as checkbox +from nextpy.interfaces.web.components import checkbox_group as checkbox_group +from nextpy.interfaces.web.components import circular_progress as circular_progress +from nextpy.interfaces.web.components import ( + circular_progress_label as circular_progress_label, +) +from nextpy.interfaces.web.components import circle as circle +from nextpy.interfaces.web.components import code as code +from nextpy.interfaces.web.components import code_block as code_block +from nextpy.interfaces.web.components import collapse as collapse +from nextpy.interfaces.web.components import color_mode_button as color_mode_button +from nextpy.interfaces.web.components import color_mode_icon as color_mode_icon +from nextpy.interfaces.web.components import color_mode_switch as color_mode_switch +from nextpy.interfaces.web.components import color_picker as color_picker +from nextpy.interfaces.web.components import component as component +from nextpy.interfaces.web.components import cond as cond +from nextpy.interfaces.web.components import connection_banner as connection_banner +from nextpy.interfaces.web.components import connection_modal as connection_modal +from nextpy.interfaces.web.components import container as container +from nextpy.interfaces.web.components import data_table as data_table +from nextpy.interfaces.web.components import data_editor as data_editor +from nextpy.interfaces.web.components import dataframe as dataframe +from nextpy.interfaces.web.components import data_editor_theme as data_editor_theme +from nextpy.interfaces.web.components import date_picker as date_picker +from nextpy.interfaces.web.components import date_time_picker as date_time_picker +from nextpy.interfaces.web.components import debounce_input as debounce_input +from nextpy.interfaces.web.components import divider as divider +from nextpy.interfaces.web.components import drawer as drawer +from nextpy.interfaces.web.components import drawer_body as drawer_body +from nextpy.interfaces.web.components import drawer_close_button as drawer_close_button +from nextpy.interfaces.web.components import drawer_content as drawer_content +from nextpy.interfaces.web.components import drawer_footer as drawer_footer +from nextpy.interfaces.web.components import drawer_header as drawer_header +from nextpy.interfaces.web.components import drawer_overlay as drawer_overlay +from nextpy.interfaces.web.components import editable as editable +from nextpy.interfaces.web.components import editable_input as editable_input +from nextpy.interfaces.web.components import editable_preview as editable_preview +from nextpy.interfaces.web.components import editable_textarea as editable_textarea +from nextpy.interfaces.web.components import editor as editor +from nextpy.interfaces.web.components import email as email +from nextpy.interfaces.web.components import empty as empty +from nextpy.interfaces.web.components import error as error +from nextpy.interfaces.web.components import expander as expander +from nextpy.interfaces.web.components import expander_button as expander_button +from nextpy.interfaces.web.components import expander_icon as expander_icon +from nextpy.interfaces.web.components import expander_item as expander_item +from nextpy.interfaces.web.components import expander_panel as expander_panel +from nextpy.interfaces.web.components import fade as fade +from nextpy.interfaces.web.components import flex as flex +from nextpy.interfaces.web.components import foreach as foreach +from nextpy.interfaces.web.components import form as form +from nextpy.interfaces.web.components import form_control as form_control +from nextpy.interfaces.web.components import form_error_message as form_error_message +from nextpy.interfaces.web.components import form_helper_text as form_helper_text +from nextpy.interfaces.web.components import form_label as form_label +from nextpy.interfaces.web.components import fragment as fragment +from nextpy.interfaces.web.components import grid as grid +from nextpy.interfaces.web.components import grid_item as grid_item +from nextpy.interfaces.web.components import header as header +from nextpy.interfaces.web.components import heading as heading +from nextpy.interfaces.web.components import highlight as highlight +from nextpy.interfaces.web.components import hstack as hstack +from nextpy.interfaces.web.components import html as html +from nextpy.interfaces.web.components import icon as icon +from nextpy.interfaces.web.components import icon_button as icon_button +from nextpy.interfaces.web.components import image as image +from nextpy.interfaces.web.components import info as info +from nextpy.interfaces.web.components import input as input +from nextpy.interfaces.web.components import input_group as input_group +from nextpy.interfaces.web.components import input_left_addon as input_left_addon +from nextpy.interfaces.web.components import input_left_element as input_left_element +from nextpy.interfaces.web.components import input_right_addon as input_right_addon +from nextpy.interfaces.web.components import input_right_element as input_right_element +from nextpy.interfaces.web.components import kbd as kbd +from nextpy.interfaces.web.components import link as link +from nextpy.interfaces.web.components import link_box as link_box +from nextpy.interfaces.web.components import link_overlay as link_overlay +from nextpy.interfaces.web.components import list as list +from nextpy.interfaces.web.components import list_item as list_item +from nextpy.interfaces.web.components import markdown as markdown +from nextpy.interfaces.web.components import match as match +from nextpy.interfaces.web.components import menu as menu +from nextpy.interfaces.web.components import menu_button as menu_button +from nextpy.interfaces.web.components import menu_divider as menu_divider +from nextpy.interfaces.web.components import menu_group as menu_group +from nextpy.interfaces.web.components import menu_item as menu_item +from nextpy.interfaces.web.components import menu_item_option as menu_item_option +from nextpy.interfaces.web.components import menu_list as menu_list +from nextpy.interfaces.web.components import menu_option_group as menu_option_group +from nextpy.interfaces.web.components import modal as modal +from nextpy.interfaces.web.components import modal_body as modal_body +from nextpy.interfaces.web.components import modal_close_button as modal_close_button +from nextpy.interfaces.web.components import modal_content as modal_content +from nextpy.interfaces.web.components import modal_footer as modal_footer +from nextpy.interfaces.web.components import modal_header as modal_header +from nextpy.interfaces.web.components import modal_overlay as modal_overlay +from nextpy.interfaces.web.components import moment as moment +from nextpy.interfaces.web.components import multi_select as multi_select +from nextpy.interfaces.web.components import multi_select_option as multi_select_option +from nextpy.interfaces.web.components import next_link as next_link +from nextpy.interfaces.web.components import ( + number_decrement_stepper as number_decrement_stepper, +) +from nextpy.interfaces.web.components import ( + number_increment_stepper as number_increment_stepper, +) +from nextpy.interfaces.web.components import number_input as number_input +from nextpy.interfaces.web.components import number_input_field as number_input_field +from nextpy.interfaces.web.components import number_input_stepper as number_input_stepper +from nextpy.interfaces.web.components import option as option +from nextpy.interfaces.web.components import ordered_list as ordered_list +from nextpy.interfaces.web.components import password as password +from nextpy.interfaces.web.components import pin_input as pin_input +from nextpy.interfaces.web.components import pin_input_field as pin_input_field +from nextpy.interfaces.web.components import plotly as plotly +from nextpy.interfaces.web.components import popover as popover +from nextpy.interfaces.web.components import popover_anchor as popover_anchor +from nextpy.interfaces.web.components import popover_arrow as popover_arrow +from nextpy.interfaces.web.components import popover_body as popover_body +from nextpy.interfaces.web.components import popover_close_button as popover_close_button +from nextpy.interfaces.web.components import popover_content as popover_content +from nextpy.interfaces.web.components import popover_footer as popover_footer +from nextpy.interfaces.web.components import popover_header as popover_header +from nextpy.interfaces.web.components import popover_trigger as popover_trigger +from nextpy.interfaces.web.components import progress as progress +from nextpy.interfaces.web.components import radio as radio +from nextpy.interfaces.web.components import radio_group as radio_group +from nextpy.interfaces.web.components import responsive_grid as responsive_grid +from nextpy.interfaces.web.components import scale_fade as scale_fade +from nextpy.interfaces.web.components import script as script +from nextpy.interfaces.web.components import select as select +from nextpy.interfaces.web.components import select_slider as select_slider +from nextpy.interfaces.web.components import ( + select_slider_filled_track as select_slider_filled_track, +) +from nextpy.interfaces.web.components import select_slider_thumb as select_slider_thumb +from nextpy.interfaces.web.components import select_slider_track as select_slider_track +from nextpy.interfaces.web.components import skeleton as skeleton +from nextpy.interfaces.web.components import skeleton_circle as skeleton_circle +from nextpy.interfaces.web.components import skeleton_text as skeleton_text +from nextpy.interfaces.web.components import slide as slide +from nextpy.interfaces.web.components import slide_fade as slide_fade +from nextpy.interfaces.web.components import slider as slider +from nextpy.interfaces.web.components import slider_filled_track as slider_filled_track +from nextpy.interfaces.web.components import slider_mark as slider_mark +from nextpy.interfaces.web.components import slider_thumb as slider_thumb +from nextpy.interfaces.web.components import slider_track as slider_track +from nextpy.interfaces.web.components import spacer as spacer +from nextpy.interfaces.web.components import span as span +from nextpy.interfaces.web.components import spinner as spinner +from nextpy.interfaces.web.components import square as square +from nextpy.interfaces.web.components import stack as stack +from nextpy.interfaces.web.components import stat as stat +from nextpy.interfaces.web.components import stat_arrow as stat_arrow +from nextpy.interfaces.web.components import stat_group as stat_group +from nextpy.interfaces.web.components import stat_help_text as stat_help_text +from nextpy.interfaces.web.components import stat_label as stat_label +from nextpy.interfaces.web.components import stat_number as stat_number +from nextpy.interfaces.web.components import step as step +from nextpy.interfaces.web.components import step_description as step_description +from nextpy.interfaces.web.components import step_icon as step_icon +from nextpy.interfaces.web.components import step_indicator as step_indicator +from nextpy.interfaces.web.components import step_number as step_number +from nextpy.interfaces.web.components import step_separator as step_separator +from nextpy.interfaces.web.components import step_status as step_status +from nextpy.interfaces.web.components import step_title as step_title +from nextpy.interfaces.web.components import stepper as stepper +from nextpy.interfaces.web.components import subheader as subheader +from nextpy.interfaces.web.components import success as success +from nextpy.interfaces.web.components import switch as switch +from nextpy.interfaces.web.components import tab as tab +from nextpy.interfaces.web.components import tab_list as tab_list +from nextpy.interfaces.web.components import tab_panel as tab_panel +from nextpy.interfaces.web.components import tab_panels as tab_panels +from nextpy.interfaces.web.components import table as table +from nextpy.interfaces.web.components import table_caption as table_caption +from nextpy.interfaces.web.components import table_container as table_container +from nextpy.interfaces.web.components import tabs as tabs +from nextpy.interfaces.web.components import tag as tag +from nextpy.interfaces.web.components import tag_close_button as tag_close_button +from nextpy.interfaces.web.components import tag_label as tag_label +from nextpy.interfaces.web.components import tag_left_icon as tag_left_icon +from nextpy.interfaces.web.components import tag_right_icon as tag_right_icon +from nextpy.interfaces.web.components import tbody as tbody +from nextpy.interfaces.web.components import td as td +from nextpy.interfaces.web.components import text as text +from nextpy.interfaces.web.components import text_area as text_area +from nextpy.interfaces.web.components import tfoot as tfoot +from nextpy.interfaces.web.components import th as th +from nextpy.interfaces.web.components import thead as thead +from nextpy.interfaces.web.components import tooltip as tooltip +from nextpy.interfaces.web.components import tr as tr +from nextpy.interfaces.web.components import unordered_list as unordered_list +from nextpy.interfaces.web.components import upload as upload +from nextpy.interfaces.web.components import video as video +from nextpy.interfaces.web.components import visually_hidden as visually_hidden +from nextpy.interfaces.web.components import vstack as vstack +from nextpy.interfaces.web.components import warning as warning +from nextpy.interfaces.web.components import wrap as wrap +from nextpy.interfaces.web.components import wrap_item as wrap_item +from nextpy.interfaces.web.components import cancel_upload as cancel_upload +from nextpy.interfaces.web import components as components +from nextpy.interfaces.web.components import color_mode_cond as color_mode_cond +from nextpy.interfaces.web.components import desktop_only as desktop_only +from nextpy.interfaces.web.components import mobile_only as mobile_only +from nextpy.interfaces.web.components import tablet_only as tablet_only +from nextpy.interfaces.web.components import mobile_and_tablet as mobile_and_tablet +from nextpy.interfaces.web.components import tablet_and_desktop as tablet_and_desktop +from nextpy.interfaces.web.components import selected_files as selected_files +from nextpy.interfaces.web.components import clear_selected_files as clear_selected_files +from nextpy.interfaces.web.components import EditorButtonList as EditorButtonList +from nextpy.interfaces.web.components import EditorOptions as EditorOptions +from nextpy.interfaces.web.components import NoSSRComponent as NoSSRComponent +from nextpy.interfaces.web.components import chakra as chakra +from nextpy.interfaces.web.components import next as next +from nextpy.interfaces.web.components.component import memo as memo +from nextpy.interfaces.web.components import recharts as recharts +from nextpy.interfaces.web.components.moment.moment import MomentDelta as MomentDelta from nextpy import config as config from nextpy.build.config import Config as Config from nextpy.build.config import DBConfig as DBConfig from nextpy import constants as constants from nextpy.constants import Env as Env -# from nextpy.frontend.custom_components import custom_components as custom_components -from nextpy.frontend.components import el as el + +# from nextpy.interfaces.custom_components import custom_components as custom_components +from nextpy.interfaces.web.components import el as el from nextpy.backend import event as event from nextpy.backend.event import EventChain as EventChain from nextpy.backend.event import background as background @@ -479,16 +509,16 @@ from nextpy.backend.middleware import Middleware as Middleware from nextpy.data import model as model from nextpy.data.model import session as session from nextpy.data.model import Model as Model -from nextpy.frontend.page import page as page +from nextpy.interfaces.web.page import page as page from nextpy.backend import route as route from nextpy.backend import state as state from nextpy.backend.state import var as var from nextpy.backend.state import Cookie as Cookie from nextpy.backend.state import LocalStorage as LocalStorage from nextpy.backend.state import State as State -from nextpy.frontend import style as style -from nextpy.frontend.style import color_mode as color_mode -from nextpy.frontend.style import toggle_color_mode as toggle_color_mode +from nextpy.interfaces.web import style as style +from nextpy.interfaces.web.style import color_mode as color_mode +from nextpy.interfaces.web.style import toggle_color_mode as toggle_color_mode from nextpy.build import testing as testing from nextpy import utils as utils from nextpy import vars as vars diff --git a/nextpy/app.py b/nextpy/app.py index a50e096d..64cdddb1 100644 --- a/nextpy/app.py +++ b/nextpy/app.py @@ -60,17 +60,17 @@ from nextpy.build.compiler.compiler import ExecutorSafeFunctions from nextpy.build.config import get_config from nextpy.data.model import Model -from nextpy.frontend.components import connection_modal -from nextpy.frontend.components.base.app_wrap import AppWrap -from nextpy.frontend.components.base.fragment import Fragment -from nextpy.frontend.components.component import Component, ComponentStyle -from nextpy.frontend.components.core.client_side_routing import ( +from nextpy.interfaces.web.components import connection_modal +from nextpy.interfaces.web.components.base.app_wrap import AppWrap +from nextpy.interfaces.web.components.base.fragment import Fragment +from nextpy.interfaces.web.components.component import Component, ComponentStyle +from nextpy.interfaces.web.components.core.client_side_routing import ( Default404Page, wait_for_client_redirect, ) -from nextpy.frontend.components.radix import themes -from nextpy.frontend.imports import ReactImportVar -from nextpy.frontend.page import ( +from nextpy.interfaces.web.components.radix import themes +from nextpy.interfaces.web.imports import ReactImportVar +from nextpy.interfaces.web.page import ( DECORATED_PAGES, ) from nextpy.utils import console, exceptions, format, types diff --git a/nextpy/app.pyi b/nextpy/app.pyi index 386a9b7e..da1e262c 100644 --- a/nextpy/app.pyi +++ b/nextpy/app.pyi @@ -11,12 +11,12 @@ from nextpy.backend.admin import AdminDash as AdminDash from nextpy.base import Base as Base from nextpy.build.compiler import compiler as compiler from nextpy.build import prerequisites as prerequisites -from nextpy.frontend.components import connection_modal as connection_modal -from nextpy.frontend.components.component import ( +from nextpy.interfaces.web.components import connection_modal as connection_modal +from nextpy.interfaces.web.components.component import ( Component as Component, ComponentStyle as ComponentStyle, ) -from nextpy.frontend.components.base.fragment import Fragment as Fragment +from nextpy.interfaces.web.components.base.fragment import Fragment as Fragment from nextpy.build.config import get_config as get_config from nextpy.backend.event import ( Event as Event, @@ -28,7 +28,7 @@ from nextpy.backend.middleware import ( Middleware as Middleware, ) from nextpy.data.model import Model as Model -from nextpy.frontend.page import DECORATED_PAGES as DECORATED_PAGES +from nextpy.interfaces.web.page import DECORATED_PAGES as DECORATED_PAGES from nextpy.backend.route import ( catchall_in_route as catchall_in_route, catchall_prefix as catchall_prefix, diff --git a/nextpy/backend/event.py b/nextpy/backend/event.py index 58421480..30225024 100644 --- a/nextpy/backend/event.py +++ b/nextpy/backend/event.py @@ -332,7 +332,7 @@ def as_event_spec(self, handler: EventHandler) -> EventSpec: Raises: ValueError: If the on_upload_progress is not a valid event handler. """ - from nextpy.frontend.components.core.upload import ( + from nextpy.interfaces.web.components.core.upload import ( DEFAULT_UPLOAD_ID, upload_files_context_var_data, ) diff --git a/nextpy/backend/vars.py b/nextpy/backend/vars.py index 9def727a..d4914723 100644 --- a/nextpy/backend/vars.py +++ b/nextpy/backend/vars.py @@ -38,10 +38,10 @@ from nextpy import constants from nextpy.base import Base -from nextpy.frontend import imports +from nextpy.interfaces.web import imports # This module used to export ReactImportVar itself, so we still import it for export here -from nextpy.frontend.imports import ImportDict, ReactImportVar +from nextpy.interfaces.web.imports import ImportDict, ReactImportVar from nextpy.utils import console, format, serializers, types if TYPE_CHECKING: diff --git a/nextpy/backend/vars.pyi b/nextpy/backend/vars.pyi index 1accb280..2581c7b8 100644 --- a/nextpy/backend/vars.pyi +++ b/nextpy/backend/vars.pyi @@ -9,7 +9,7 @@ from nextpy.base import Base as Base from nextpy.backend.state import State as State from nextpy.backend.state import BaseState as BaseState from nextpy.utils import console as console, format as format, types as types -from nextpy.frontend.imports import ReactImportVar +from nextpy.interfaces.web.imports import ReactImportVar from types import FunctionType from typing import ( Any, diff --git a/nextpy/build/compiler/compiler.py b/nextpy/build/compiler/compiler.py index 1bbc2216..a83ff780 100644 --- a/nextpy/build/compiler/compiler.py +++ b/nextpy/build/compiler/compiler.py @@ -10,7 +10,7 @@ from nextpy import constants from nextpy.build.compiler import templates, utils -from nextpy.frontend.components.component import ( +from nextpy.interfaces.web.components.component import ( BaseComponent, Component, ComponentStyle, @@ -19,7 +19,7 @@ ) from nextpy.build.config import get_config from nextpy.backend.state import BaseState -from nextpy.frontend.imports import ImportDict, ReactImportVar +from nextpy.interfaces.web.imports import ImportDict, ReactImportVar def _compile_document_root(root: Component) -> str: diff --git a/nextpy/build/compiler/utils.py b/nextpy/build/compiler/utils.py index 7cc15e9b..6a80b59a 100644 --- a/nextpy/build/compiler/utils.py +++ b/nextpy/build/compiler/utils.py @@ -11,7 +11,7 @@ from pydantic.fields import ModelField from nextpy import constants -from nextpy.frontend.components.base import ( +from nextpy.interfaces.web.components.base import ( Body, Description, DocumentHead, @@ -23,11 +23,11 @@ NextScript, Title, ) -from nextpy.frontend.components.component import Component, ComponentStyle, CustomComponent +from nextpy.interfaces.web.components.component import Component, ComponentStyle, CustomComponent from nextpy.backend.state import BaseState, Cookie, LocalStorage -from nextpy.frontend.style import Style +from nextpy.interfaces.web.style import Style from nextpy.utils import console, path_ops -from nextpy.frontend import imports +from nextpy.interfaces.web import imports from nextpy.utils import format # To re-export this function. diff --git a/nextpy/cli.py b/nextpy/cli.py index 195eb498..fca806f3 100644 --- a/nextpy/cli.py +++ b/nextpy/cli.py @@ -24,7 +24,7 @@ from nextpy import constants from nextpy.build import dependency from nextpy.build.config import get_config -from nextpy.frontend.custom_components.custom_components import custom_components_cli +from nextpy.interfaces.custom_components.custom_components import custom_components_cli from nextpy.utils import console, telemetry # Disable typer+rich integration for help panels diff --git a/nextpy/constants/base.py b/nextpy/constants/base.py index a62e27aa..90fc263a 100644 --- a/nextpy/constants/base.py +++ b/nextpy/constants/base.py @@ -80,7 +80,7 @@ class Templates(SimpleNamespace): # Dynamically get the enum values from the templates folder template_dir = os.path.join( - Nextpy.ROOT_DIR, Nextpy.MODULE_NAME, "frontend/templates/apps" + Nextpy.ROOT_DIR, Nextpy.MODULE_NAME, "interfaces/templates/apps" ) template_dirs = next(os.walk(template_dir))[1] @@ -91,7 +91,7 @@ class Dirs(SimpleNamespace): """Folders used by the template system of Nextpy.""" # The template directory used during nextpy init. - BASE = os.path.join(Nextpy.ROOT_DIR, Nextpy.MODULE_NAME, "frontend/templates") + BASE = os.path.join(Nextpy.ROOT_DIR, Nextpy.MODULE_NAME, "interfaces/templates") # The web subdirectory of the template directory. WEB_TEMPLATE = os.path.join(BASE, "web") # The jinja template directory. diff --git a/nextpy/constants/compiler.py b/nextpy/constants/compiler.py index 01350509..90ff6708 100644 --- a/nextpy/constants/compiler.py +++ b/nextpy/constants/compiler.py @@ -8,7 +8,7 @@ from nextpy.base import Base from nextpy.constants import Dirs -from nextpy.frontend.imports import ReactImportVar +from nextpy.interfaces.web.imports import ReactImportVar # The prefix used to create setters for state vars. SETTER_PREFIX = "set_" diff --git a/nextpy/frontend/components/chakra/disclosure/accordion.pyi b/nextpy/frontend/components/chakra/disclosure/accordion.pyi deleted file mode 100644 index 15f96c70..00000000 --- a/nextpy/frontend/components/chakra/disclosure/accordion.pyi +++ /dev/null @@ -1,432 +0,0 @@ -# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. -# We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. - -"""Stub file for nextpy/components/chakra/disclosure/accordion.py""" -# ------------------- DO NOT EDIT ---------------------- -# This file was generated by `scripts/pyi_generator.py`! -# ------------------------------------------------------ - -from typing import Any, Dict, Literal, Optional, Union, overload -from nextpy.backend.vars import Var, BaseVar, ComputedVar -from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style -from typing import List, Optional, Union -from nextpy.frontend.components.chakra import ChakraComponent -from nextpy.frontend.components.component import Component -from nextpy.backend.vars import Var - -class Accordion(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - items=None, - icon_pos="right", - allow_multiple: Optional[Union[Var[bool], bool]] = None, - allow_toggle: Optional[Union[Var[bool], bool]] = None, - default_index: Optional[ - Union[Var[Optional[List[int]]], Optional[List[int]]] - ] = None, - index: Optional[ - Union[Var[Union[int, List[int]]], Union[int, List[int]]] - ] = None, - reduce_motion: Optional[Union[Var[bool], bool]] = None, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "Accordion": - """Create an accordion component. - - Args: - *children: The children of the component. - items: The items of the accordion component: list of tuples (label,panel) - icon_pos: The position of the arrow icon of the accordion. "right", "left" or None - allow_multiple: The allow_multiple property of the accordion. (True or False) - allow_toggle: The allow_toggle property of the accordion. (True or False) - default_index: The initial index(es) of the expanded accordion item(s). - index: The index(es) of the expanded accordion item - reduce_motion: If true, height animation and transitions will be disabled. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The properties of the component. - - Returns: - The accordion component - """ - ... - -class AccordionItem(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - id_: Optional[Union[Var[str], str]] = None, - is_disabled: Optional[Union[Var[bool], bool]] = None, - is_focusable: Optional[Union[Var[bool], bool]] = None, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "AccordionItem": - """Create the component. - - Args: - *children: The children of the component. - id_: A unique id for the accordion item. - is_disabled: If true, the accordion item will be disabled. - is_focusable: If true, the accordion item will be focusable. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... - -class AccordionButton(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "AccordionButton": - """Create the component. - - Args: - *children: The children of the component. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... - -class AccordionPanel(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "AccordionPanel": - """Create the component. - - Args: - *children: The children of the component. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... - -class AccordionIcon(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "AccordionIcon": - """Create the component. - - Args: - *children: The children of the component. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... diff --git a/nextpy/frontend/components/chakra/feedback/alert.py b/nextpy/frontend/components/chakra/feedback/alert.py deleted file mode 100644 index 7ea85b7a..00000000 --- a/nextpy/frontend/components/chakra/feedback/alert.py +++ /dev/null @@ -1,71 +0,0 @@ -# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. -# We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. - -"""Alert components.""" - -from nextpy.backend.vars import Var -from nextpy.frontend.components.chakra import ( - ChakraComponent, - LiteralAlertVariant, - LiteralStatus, -) -from nextpy.frontend.components.component import Component - - -class Alert(ChakraComponent): - """An alert feedback box.""" - - tag = "Alert" - - # The status of the alert ("success" | "info" | "warning" | "error") - status: Var[LiteralStatus] - - # "subtle" | "left-accent" | "top-accent" | "solid" - variant: Var[LiteralAlertVariant] - - @classmethod - def create( - cls, *children, icon=True, title="Alert title", desc=None, **props - ) -> Component: - """Create an alert component. - - Args: - *children: The children of the component. - icon: The icon of the alert. - title: The title of the alert. - desc: The description of the alert - **props: The properties of the component. - - Returns: - The alert component. - """ - if len(children) == 0: - children = [] - - if icon: - children.append(AlertIcon.create()) - - children.append(AlertTitle.create(title)) - - if desc: - children.append(AlertDescription.create(desc)) - - return super().create(*children, **props) - - -class AlertIcon(ChakraComponent): - """An icon displayed in the alert.""" - - tag = "AlertIcon" - - -class AlertTitle(ChakraComponent): - """The title of the alert.""" - - tag = "AlertTitle" - - -class AlertDescription(ChakraComponent): - """AlertDescription composes the Box component.""" - - tag = "AlertDescription" diff --git a/nextpy/frontend/components/chakra/feedback/alert.pyi b/nextpy/frontend/components/chakra/feedback/alert.pyi deleted file mode 100644 index aecbc7a2..00000000 --- a/nextpy/frontend/components/chakra/feedback/alert.pyi +++ /dev/null @@ -1,348 +0,0 @@ -# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. -# We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. - -"""Stub file for nextpy/components/chakra/feedback/alert.py""" -# ------------------- DO NOT EDIT ---------------------- -# This file was generated by `scripts/pyi_generator.py`! -# ------------------------------------------------------ - -from typing import Any, Dict, Literal, Optional, Union, overload -from nextpy.backend.vars import Var, BaseVar, ComputedVar -from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style -from nextpy.frontend.components.chakra import ChakraComponent, LiteralAlertVariant, LiteralStatus -from nextpy.frontend.components.component import Component -from nextpy.backend.vars import Var - -class Alert(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - icon=True, - title="Alert title", - desc=None, - status: Optional[ - Union[ - Var[Literal["success", "info", "warning", "error"]], - Literal["success", "info", "warning", "error"], - ] - ] = None, - variant: Optional[ - Union[ - Var[Literal["subtle", "left-accent", "top-accent", "solid"]], - Literal["subtle", "left-accent", "top-accent", "solid"], - ] - ] = None, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "Alert": - """Create an alert component. - - Args: - *children: The children of the component. - icon: The icon of the alert. - title: The title of the alert. - desc: The description of the alert - status: The status of the alert ("success" | "info" | "warning" | "error") - variant: "subtle" | "left-accent" | "top-accent" | "solid" - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The properties of the component. - - Returns: - The alert component. - """ - ... - -class AlertIcon(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "AlertIcon": - """Create the component. - - Args: - *children: The children of the component. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... - -class AlertTitle(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "AlertTitle": - """Create the component. - - Args: - *children: The children of the component. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... - -class AlertDescription(ChakraComponent): - @overload - @classmethod - def create( # type: ignore - cls, - *children, - style: Optional[Style] = None, - key: Optional[Any] = None, - id: Optional[Any] = None, - class_name: Optional[Any] = None, - autofocus: Optional[bool] = None, - custom_attrs: Optional[Dict[str, Union[Var, str]]] = None, - on_blur: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_context_menu: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_double_click: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_focus: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_down: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_enter: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_leave: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_move: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_out: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_over: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_mouse_up: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_scroll: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - on_unmount: Optional[ - Union[EventHandler, EventSpec, list, function, BaseVar] - ] = None, - **props - ) -> "AlertDescription": - """Create the component. - - Args: - *children: The children of the component. - style: The style of the component. - key: A unique key for the component. - id: The id for the component. - class_name: The class name for the component. - autofocus: Whether the component should take the focus once the page is loaded - custom_attrs: custom attribute - **props: The props of the component. - - Returns: - The component. - - Raises: - TypeError: If an invalid child is passed. - """ - ... diff --git a/nextpy/frontend/components/media/icon.py b/nextpy/frontend/components/media/icon.py deleted file mode 100644 index ce426f0d..00000000 --- a/nextpy/frontend/components/media/icon.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file has been modified by the Nextpy Team in 2023 using AI tools and automation scripts. -# We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. - -"""Shim for nextpy.frontend.components.chakra.media.icon.""" -from nextpy.frontend.components.chakra.media.icon import * diff --git a/nextpy/frontend/__init__.py b/nextpy/interfaces/__init__.py similarity index 100% rename from nextpy/frontend/__init__.py rename to nextpy/interfaces/__init__.py diff --git a/nextpy/frontend/blueprints/__init__.py b/nextpy/interfaces/blueprints/__init__.py similarity index 100% rename from nextpy/frontend/blueprints/__init__.py rename to nextpy/interfaces/blueprints/__init__.py diff --git a/nextpy/frontend/blueprints/hero_section.py b/nextpy/interfaces/blueprints/hero_section.py similarity index 100% rename from nextpy/frontend/blueprints/hero_section.py rename to nextpy/interfaces/blueprints/hero_section.py diff --git a/nextpy/frontend/blueprints/navbar.py b/nextpy/interfaces/blueprints/navbar.py similarity index 100% rename from nextpy/frontend/blueprints/navbar.py rename to nextpy/interfaces/blueprints/navbar.py diff --git a/nextpy/frontend/blueprints/sidebar.py b/nextpy/interfaces/blueprints/sidebar.py similarity index 100% rename from nextpy/frontend/blueprints/sidebar.py rename to nextpy/interfaces/blueprints/sidebar.py diff --git a/nextpy/frontend/custom_components/__init__.py b/nextpy/interfaces/custom_components/__init__.py similarity index 100% rename from nextpy/frontend/custom_components/__init__.py rename to nextpy/interfaces/custom_components/__init__.py diff --git a/nextpy/frontend/custom_components/custom_components.py b/nextpy/interfaces/custom_components/custom_components.py similarity index 100% rename from nextpy/frontend/custom_components/custom_components.py rename to nextpy/interfaces/custom_components/custom_components.py diff --git a/nextpy/interfaces/jupyter/__init__.py b/nextpy/interfaces/jupyter/__init__.py new file mode 100644 index 00000000..4c20ea06 --- /dev/null +++ b/nextpy/interfaces/jupyter/__init__.py @@ -0,0 +1,114 @@ + + +from . import comm # noqa: F401 + + +def _using_solara_server(): + import sys + + if "widget.server" in sys.modules: + return True + if sys.argv[0].split("/")[-1] == "widget": + return True + return False + + +# isort: skip_file +from reacton import ( + component, + component_interactive, + value_component, + create_context, + get_widget, + get_context, + make, + provide_context, + render, + render_fixed, + use_context, + use_effect, + use_exception, + use_memo, + use_reducer, + use_ref, + use_side_effect, + use_state, + use_state_widget, +) # noqa: F403, F401 +from reacton.core import Element # noqa: F403, F401 + +try: + import ipyvuetify.components as v # type: ignore # noqa: F401 +except ModuleNotFoundError: + # backwards compatibility + import reacton.ipyvuetify as v # type: ignore # noqa: F401 +from . import util + +from .reactive import * + +# flake8: noqa: F402 +from .datatypes import * +from .hooks import * +from .cache import memoize +from . import cache +from .components import * +from .state import State +from .routing import use_route, use_router, use_route_level, find_route, use_pathname, resolve_path +from .checks import check_jupyter + + +def display(*objs, **kwargs): + """Display a Python object in the current component. + + ## Supported objects + + Any object that can be displayed in the Jupyter notebook can be displayed in Solara + as well. This means that plotly, pandas, vaex and other libraries that have + a display function will work out of the box in Solara. + + However, if you require callback functions, use the specific Solara components, e.g.: + + * [Plotly](/api/plotly) + * [Altair](/api/altair) + * [Matplotlib](/api/matplotlib) + * [Dataframe](/api/dataframe) + + ```widget + import nextpy.interfaces.jupyter + import pandas as pd + import plotly.express as px + + df = px.data.gapminder() + fig = px.scatter(df, x="gdpPercap", y="lifeExp") + button = widget.button("Click me") + + @widget.component + def Page(): + + # You can use widget.display directly + widget.display(fig) + + # Or use the IPython display function, which is in the global scope + # in Jupyter and Solara + display(df) + + with widget.Card("Some card"): + # you can use display to add existing elements to the parent's + # children + display(button) + ``` + Note that this is a dispatch call to the + [IPython display function](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.display), + with a slimmer API. The main purpose of this function is the have type safety, since tools like mypy do now know about + the existence of the IPython display function, and will complain if you use it without importing it. + Since widget is always imported, this saves you from having to import IPython in your code. + + ## Arguments + + * `objs`: The objects to display. + * `kwargs`: Keyword arguments to pass to the IPython display function. + + """ + from IPython.display import display as ipy_display + + ipy_display(*objs, **kwargs) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/alias.py b/nextpy/interfaces/jupyter/alias.py new file mode 100644 index 00000000..b36b61f6 --- /dev/null +++ b/nextpy/interfaces/jupyter/alias.py @@ -0,0 +1,6 @@ +import reacton # noqa: F401 +from reacton import ipyvue as rvue # noqa: F401 +from reacton import ipyvuetify as rv # noqa: F401 +from reacton import ipywidgets as rw # noqa: F401 + +import nextpy.interfaces.jupyter as widget # noqa: F401 diff --git a/nextpy/interfaces/jupyter/cache.py b/nextpy/interfaces/jupyter/cache.py new file mode 100644 index 00000000..8c364550 --- /dev/null +++ b/nextpy/interfaces/jupyter/cache.py @@ -0,0 +1,293 @@ +import hashlib +import inspect +import logging +import sys +from typing import ( + Any, + Callable, + Dict, + Generic, + MutableMapping, + Optional, + TypeVar, + Union, + cast, + overload, +) + +import cachetools +import nextpy.interfaces.jupyter as widget +import nextpy.interfaces.jupyter.settings +import nextpy.interfaces.jupyter.util +import typing_extensions +from reacton.utils import equals + +logger = logging.getLogger("widget.cache") + +# placeholder value for missing keys +_DOES_NOT_EXIST = object() + +_global_values_used: Dict[Any, Dict[str, Any]] = {} + + +T = TypeVar("T") +R = TypeVar("R") +K = TypeVar("K") +V = TypeVar("V") +P = typing_extensions.ParamSpec("P") + +Storage = MutableMapping[K, V] + + +class Memory(cachetools.LRUCache): + def __init__(self, max_items=widget.settings.cache.memory_max_items): + super().__init__(maxsize=max_items) + + +def _default_key(*args, **kwargs): + kwargs_tuple: Any = () + for key, value in kwargs.items(): + kwargs_tuple += (key, value) + return (args, kwargs_tuple) + + +class MemoizedFunction(Generic[P, R]): + def __init__(self, function: Callable[P, R], key: Callable[P, R], storage: Optional[Storage], allow_nonlocals=False): + self.function = function + f: Callable = self.function + if not allow_nonlocals: + nonlocals = inspect.getclosurevars(f).nonlocals + if nonlocals: + raise ValueError(f"Memoized functions cannot depend on nonlocal variables, it now depends on {nonlocals}") + if sys.version_info[:2] < (3, 9): + # usedforsecurity is only available in Python 3.9+ + codehash = hashlib.md5(f.__code__.co_code).hexdigest() + else: + codehash = hashlib.md5(f.__code__.co_code, usedforsecurity=False).hexdigest() # type: ignore + + self.function_key = (f.__qualname__, codehash) + current_globals = dict(inspect.getclosurevars(f).globals) + _global_values_used.setdefault(self.function_key, current_globals) + self._check_globals() + self.key = key + self._storage = storage + self.intrusive_cancel = True + # modifications to these are not thread safe + # but good enough for an indication + self.hits = 0 + self.misses = 0 + + @property + def storage(self) -> Storage: + return self._storage if self._storage is not None else storage + + def _check_globals(self): + globals = _global_values_used.get(self.function_key) + current_globals = dict(inspect.getclosurevars(self.function).globals) + if current_globals is not globals: + if not equals(current_globals, globals): + raise ValueError( + f"Memoized functions depend on globals variables, but they changed. The first value was {globals}, but now it is {current_globals}" + ) + + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: + self._check_globals() + key = (self.function_key, self.key(*args, **kwargs)) + # if .get is atomic, we do not need to lock + value = cast(R, self.storage.get(key, _DOES_NOT_EXIST)) + if value is _DOES_NOT_EXIST: + self.misses += 1 + value = self.function(*args, **kwargs) + self.storage[key] = value + else: + self.hits += 1 + return value + + def use_thread(self, *args: P.args, **kwargs: P.kwargs) -> widget.Result[R]: + """Calls the function in a thread when not in cache, otherwise returns the cached value. + + This is almost similar to a use_thread on the memoized function, except that + we ensure that if the function return value is already in the cache, we don't + have to wait for the thread to finish. + + This can cause less flickering in the UI, since it will immediately return a + 'FINISHED' result, instead of a 'RUNNING' result when possible. + """ + key = (self.function_key, self.key(*args, **kwargs)) + + # we assume generating the key is cheap, but a storage.get might not be + # since it may have to ask the a server, so we wrap it in use_memo + def get_current(): + return cast(R, self.storage.get(key, _DOES_NOT_EXIST)) + + value = widget.use_memo(get_current, dependencies=[key]) + + def do_work(): + if value is _DOES_NOT_EXIST: + self.misses += 1 + self._check_globals() + new_value = self.function(*args, **kwargs) + # TODO: what if someone else already put in the value? + # we do a check in vaex if it differs + # but we could also store a Future + self.storage[key] = new_value + return new_value + else: + # although we don't use the return value directly, it's still used on the next time result_thread is + # returned. + return value + + result_thread: widget.Result[R] = widget.use_thread(do_work, dependencies=[key], intrusive_cancel=self.intrusive_cancel) + if value is _DOES_NOT_EXIST: + return result_thread + else: + self.hits += 1 + # what are the semantics of cancel and retry here? + return widget.Result(value, state=widget.ResultState.FINISHED) + + +@overload +def memoize( + function: None = None, + key: None = None, + storage: Optional[Storage] = None, + allow_nonlocals=False, +) -> Callable[[Callable[P, R]], MemoizedFunction[P, R]]: + ... + + +@overload +def memoize( + function: None = None, + key: Callable[P, R] = ..., + storage: Optional[Storage] = None, + allow_nonlocals=False, +) -> Callable[[Callable[P, R]], MemoizedFunction[P, R]]: + ... + + +@overload +def memoize( + function: Callable[P, R], + key: None = None, + storage: None = None, + allow_nonlocals=False, +) -> MemoizedFunction[P, R]: + ... + + +def memoize( + function: Union[None, Callable[P, R]] = None, + key: Union[None, Callable[P, R]] = None, + storage: Optional[Storage] = None, + allow_nonlocals: bool = False, +) -> Union[Callable[[Callable[P, R]], MemoizedFunction[P, R]], MemoizedFunction[P, R]]: + """Will cache function return values based on the arguments. + + Example: + + ```python + @widget.memoize + def mean(values): + return sum(values) / len(values) + ``` + + If a key function is provided, it will be used to generate the cache key based on the arguments instead. + This is useful in situations where the arguments are not hashable, but a unique key can be generated + based on the arguments. + + Also used in situations where the arguments are expensive to hash, but a cheaper key + that is unique exists. For example using the filename instead of the file content. + + Example: + + ```python + @widget.memoize(key=lambda df, column: (id(df), column)) + def mean(df, column): + return df[column].mean() + ``` + + Without the key function, the above would fail because a DataFrame is not hashable. + + The function name and function code are added to the argument based key, together making up a unique cache key. + Nonlocals variables are not included in the key, and are not allowed by default. Pass `allow_nonlocals=True` + to allow nonlocals variables, or add them as arguments to the function. + Globals are by default allowed, and also not included in the key. Solara will try to detect if globals are + changed, and if so, will raise a ValueError. + + If a storage is provided, it will be used to store the cached values. If no storage is provided, the + shared storage will be used. This is useful in situations where the cache should be shared between + different functions to avoid excessive memory usage by limiting the number of cache entries or the + memory content. + + The storage can be any object that implements the MutableMapping interface, for instance a dict or + a cachetools.LRUCache. Or a new instance of `widget.cache.Memory`, see [caching](/docs/reference/caching) + for cache storage options. + + The return value of the decorator behaves like the original function, but also has a few attributes: + + * `storage`: the storage used to cache the values + * `key`: the key function used to generate the key + * `function`: the original function + * `use_thread`: a hook that will execute the function in a thread, and return a Result object + If the value is already cached, the function will not be executed in a thread. + + + See also the [reference on caching](/docs/reference/caching) for more caching details. + + """ + + def wrapper(func: Callable[P, R]) -> MemoizedFunction[P, R]: + return MemoizedFunction[P, R]( + func, + cast(Callable[P, R], key or _default_key), + storage, + allow_nonlocals, + ) + + if function is None: + return wrapper + else: + return wrapper(function) + + +cache_type_map = { + "memory": "nextpy.interfaces.jupyter.cache.Memory", + "memory-size": "solara_enterprise.cache.memory_size.MemorySize", + "disk": "solara_enterprise.cache.disk.Disk", + "redis": "solara_enterprise.cache.redis.Redis", + "multi-level": "solara_enterprise.cache.multi_level.MultiLevel", +} + + +def create(name: str = "memory", *args, **kwargs) -> Storage: + """Create a cache storage based on the name. + + ## Arguments + + - `name`: the name of the cache type, can be one of `memory`, `memory-size`, `disk`, `redis`, `multi-level` + - `*args`: the arguments to pass to the cache constructor + - `**kwargs`: the keyword arguments to pass to the cache constructor + + """ + if "," in name: + names = name.split(",") + logger.info("Set multilevel cache to %r", names) + caches = [] + for name in names: + caches.append(create(name, *args, **kwargs)) + cache = create("multi-level", *caches) + elif name in cache_type_map: + cls = widget.util.import_item(cache_type_map[name]) + cache = cls(*args, **kwargs) + else: + raise ValueError(f"Unknown type of cache {name}") + return cache + +storage: Storage = create(widget.settings.cache.type) + + +def configure(name="memory", *args, **kwargs): + """Shorthand for widget.cache.storage = widget.cache.create(name, *args, **kwargs)""" + global storage + storage = create(name, *args, **kwargs) diff --git a/nextpy/interfaces/jupyter/checks.html b/nextpy/interfaces/jupyter/checks.html new file mode 100644 index 00000000..fa327ef9 --- /dev/null +++ b/nextpy/interfaces/jupyter/checks.html @@ -0,0 +1,71 @@ +
+ +
diff --git a/nextpy/interfaces/jupyter/checks.py b/nextpy/interfaces/jupyter/checks.py new file mode 100644 index 00000000..7642fc54 --- /dev/null +++ b/nextpy/interfaces/jupyter/checks.py @@ -0,0 +1,224 @@ +import json +import logging +import os +import subprocess +import sys +import warnings +from pathlib import Path +from typing import Optional + +import IPython.display +from IPython.core.interactiveshell import InteractiveShell +from IPython.display import display + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.util import get_solara_home + +HERE = Path(__file__).parent +logger = logging.getLogger(__name__) + +jupyter_checked_path = get_solara_home() / ".jupyter_checked" +solara_checked_path = get_solara_home() / ".solara_checked" +# solara_version = widget.__version__ + + +def _should_perform_check(path: Path): + if path.exists(): + return False + try: + home = get_solara_home() + if not home.exists(): + home.mkdir(parents=True, exist_ok=True) + # try writing, if we cannot, we will not check + if not path.exists(): + path.write_text("") + path.unlink() + except OSError: + return False + return True + + +def should_perform_jupyter_check(): + if "PYTEST_CURRENT_TEST" in os.environ: + return False + return _should_perform_check(jupyter_checked_path) + + +def should_perform_solara_check(): + if "PYTEST_CURRENT_TEST" in os.environ: + return False + import nextpy.interfaces.jupyter.server.settings + + if widget.server.settings.main.mode == "production": + return False + return _should_perform_check(solara_checked_path) + + +# @widget.component +# def JupyterCheck(): +# # We do 2 calls home: +# # * 1 from pure js (should always work) +# # * 1 from a widget (might not work) +# # This way we can see how many installations actually fail +# # Note that we only do this once, we touch ~/.widget/.jupyter_checked +# # to avoid doing it multiple times +# def inject_pure_js_check(): +# # do this as an effect, otherwise we will use the display +# # that gets dispatched to widget, which will not come through +# # if the widgets do not work +# IPython.display.display( +# IPython.display.Javascript( +# data=f""" +# const prevIframe = document.getElementById("widget-jupyter-check"); +# if(prevIframe) +# prevIframe.remove(); +# const iframe = document.createElement('iframe') +# iframe.setAttribute("src", "https://widget.dev/static/public/success.html?check=purejs&version={1}"); +# iframe.style.width = "0px"; +# iframe.style.height = "0px"; +# iframe.style.display = "none"; +# iframe.id = "widget-jupyter-check"; +# document.body.appendChild(iframe); +# """ +# ) +# ) + +# # this should always get through, even if widgets do not work +# widget.use_effect(inject_pure_js_check, []) + +# def flag_jupyter_checked(): +# try: +# jupyter_checked_path.write_text("") +# except OSError: +# pass + +# widget.use_effect(flag_jupyter_checked, []) +# # this iframe should only get through if the widget installation succeeded +# return widget.v.Html( +# tag="iframe", +# attributes={"src": f"https://widget.dev/static/public/success.html?check=widget&version={1}", "width": "0px", "height": "0px"}, +# style_="display: none;", +# ) + + +@widget.component +def SolaraCheck(): + def flag_solara_checked(): + try: + solara_checked_path.write_text("") + except OSError: + pass + + widget.use_effect(flag_solara_checked, []) + return widget.v.Html( + tag="iframe", + attributes={ + "src": f"https://widget.dev/static/public/success.html?system=widget&check=widget&version={solara_version}", + "width": "0px", + "height": "0px", + }, + style_="display: none;", + ) + + +def getcmdline(pid): + # for linux + if sys.platform == "linux": + with open(f"/proc/{pid}/cmdline", "rb") as f: + return f.read().split(b"\00")[0].decode("utf-8") + elif sys.platform == "darwin": + return subprocess.check_output(["ps", "-o", "command=", "-p", str(pid)]).split(b"\n")[0].split(b" ")[0].decode("utf-8") + elif sys.platform == "win32": + return subprocess.check_output(["wmic", "process", "get", "commandline", "/format:list"]).split(b"\n")[0].split(b" ")[0].decode("utf-8") + else: + raise ValueError(f"Unsupported platform: {sys.platform}") + + +def get_server_python_executable(silent: bool = False): + servers = [] + try: + from jupyter_server import serverapp + + servers += list(serverapp.list_running_servers()) + except ImportError: + pass + try: + from notebook import notebookapp + + servers += list(notebookapp.list_running_servers()) + except ImportError: + pass + + pythons = [getcmdline(server["pid"]) for server in servers] + if len(pythons) == 0: + python = sys.executable + if not silent: + warnings.warn("Could not find servers, we are assuming the server is running under Python executable: %s" % python) + elif len(pythons) > 1: + info = "\n\t".join(pythons) + if sys.executable in pythons: + python = sys.executable + else: + python = pythons[0] + if not silent: + warnings.warn("Found multiple find servers:\n%s\n" "We are assuming the server is running under Python executable: %s" % (info, python)) + else: + python = pythons[0] + return python + + +libraries_minimal = [ + {"python": "ipyvuetify", "classic": "jupyter-vuetify/extension", "lab": "jupyter-vuetify"}, + {"python": "ipyvue", "classic": "jupyter-vue/extension", "lab": "jupyter-vue"}, +] + +libraries_extra = [ + {"python": "bqplot", "classic": "bqplot/extension", "lab": "bqplot"}, + {"python": "ipyvolume", "classic": "ipyvolume/extension", "lab": "ipyvolume"}, + {"python": "ipywebrtc", "classic": "jupyter-webrtc", "lab": "jupyter-webrtc"}, + {"python": "ipyleaflet", "classic": "ipyleaflet/extension", "lab": "ipyleaflet"}, +] + + +def check_jupyter( + server_python: Optional[str] = None, + silent: bool = False, + libraries: list = libraries_minimal, + libraries_extra: list = libraries_extra, + force: bool = False, + extra: bool = False, +): + if widget._using_solara_server(): + # for the server we don't need to do this check + return + if not InteractiveShell.initialized(): + # also, in a normal python repr, we don't want to display anything + return + try: + python_executable = server_python or get_server_python_executable(silent) + if Path(python_executable).resolve() != Path(sys.executable).resolve() or force: + libraries_json = json.dumps(libraries + (libraries_extra if extra else [])) + display( + IPython.display.Javascript( + data=""" + window.jupyter_python_executable = %r; + window.jupyter_widget_checks_silent = %s; + window.jupyter_widget_checks_libraries = %s; + """ + % (python_executable, str(silent).lower(), libraries_json) + ) + ) + display(IPython.display.html(filename=str(HERE / "checks.html"))) + else: + if not silent: + display( + IPython.display.html( + data="
Jupyter server is running under the same Python executable as your kernel, no need to check 👍." + "
Run widget.check_jupyter(force=True) to force checking.
" + ) + ) + except Exception: + logger.exception("Could not check jupyter-widgets extensions.") + + +check_jupyter(silent=True) diff --git a/nextpy/interfaces/jupyter/comm.py b/nextpy/interfaces/jupyter/comm.py new file mode 100644 index 00000000..a2c0c224 --- /dev/null +++ b/nextpy/interfaces/jupyter/comm.py @@ -0,0 +1,28 @@ +import traceback +from typing import Any, Dict + +try: + import comm +except ImportError: + comm = None # type: ignore + +orphan_comm_stacks: Dict[Any, str] = {} + + +if comm is not None and comm.create_comm is comm._create_comm: + # only when nobody else has monkey-patched comm.create_comm + class DummyComm(comm.base_comm.BaseComm): # type: ignore + def publish_msg(self, msg_type, data=None, metadata=None, buffers=None, **keys): + pass + + def create_dummy_comm(*args, **kwargs): + comm = DummyComm(*args, **kwargs) + stacktrace = "".join(traceback.format_stack()) + orphan_comm_stacks[comm] = stacktrace + return comm + + comm.create_comm = create_dummy_comm +else: + + class DummyComm: # type: ignore + pass diff --git a/nextpy/interfaces/jupyter/components/__init__.py b/nextpy/interfaces/jupyter/components/__init__.py new file mode 100644 index 00000000..9f464c7b --- /dev/null +++ b/nextpy/interfaces/jupyter/components/__init__.py @@ -0,0 +1,21 @@ +from .applayout import AppLayout, Sidebar, AppBar, AppBarTitle +from .misc import * +import reacton.core +reacton.core._default_container = column + +from .chat_element import chat_input +from .chart import matplotlib +from .containers import box, card, grid, responsive_grid, sidebar, tabs, expander, add_content_to_expander, create_container_with_layout_and_card +from .control import form +from .data_elements import dataframe, metrics, json_display +from .input_widgets import button, download_button, link_button +from .input_widgets import checkbox, select_box, multiselect, radio, switch +from .input_widgets import select_slider, select_slider_float +from .input_widgets import slider, slider_date, slider_float, slider_value +from .input_widgets import file_browser, file_dropper, input, input_float, input_int, textarea +from .input_widgets import color_picker, date_picker, time_picker +from .media import audio, image, video +from .status import error, info, progress, spinner, success, warning +from .style import style +from .typography import caption, code, divider, markdown, text, title, tooltip +from .typography import header, h1, h2, h3, h4, h5, h6, subheader diff --git a/nextpy/interfaces/jupyter/components/applayout.py b/nextpy/interfaces/jupyter/components/applayout.py new file mode 100644 index 00000000..f4ead077 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/applayout.py @@ -0,0 +1,397 @@ +from typing import Callable, Dict, List, Optional, Tuple, Union, cast + +import reacton +import reacton.core +import reacton.ipyvuetify as v +import reacton.utils +from reacton.core import Element + +import nextpy.interfaces.jupyter as widget + + +from .typography import title_file as t + + +@widget.component +def AppIcon(open=False, on_click=None, **kwargs): + def click(*ignore): + on_click() + + icon = v.AppBarNavIcon(**kwargs) + v.use_event(icon, "click", click) + return icon + + +should_use_embed = widget.create_context(False) +PortalElements = Dict[str, List[Tuple[int, Element]]] + + +def _set_sidebar_default(updater: Callable[[PortalElements], PortalElements]): + pass + + +class ElementPortal: + def __init__(self): + self.context = widget.create_context(_set_sidebar_default) + + # TODO: can we generalize the use of 'portals' ? (i.e. transporting elements from one place to another) + def use_portal(self) -> List[Element]: + portal_elements, set_portal_elements = widget.use_state(cast(PortalElements, {})) + self.context.provide(set_portal_elements) # type: ignore + + portal_elements_flat: List[Tuple[int, Element]] = [] + for uuid, value in portal_elements.items(): + portal_elements_flat.extend(value) + portal_elements_flat.sort(key=lambda x: x[0]) + return [e[1] for e in portal_elements_flat] + + def use_portal_add(self, children: List[Element], offset: int): + key = widget.use_unique_key(prefix="portal-") + set_portal_elements = widget.use_context(self.context) + values: List[Tuple[int, Element]] = [] + for i, child in enumerate(children): + values.append((offset + i, child)) + + # updates we do when children/offset changes + def add(): + # we use the update function method, to avoid stale data + def update_dict(portal_elements): + portal_elements_updated = portal_elements.copy() + portal_elements_updated[key] = values + return portal_elements_updated + + set_portal_elements(update_dict) + + widget.use_effect(add, [values]) + + # cleanup we only need to do after component removal + def add_cleanup(): + def cleanup(): + def without(portal_elements): + portal_elements_restored = portal_elements.copy() + portal_elements_restored.pop(key, None) + return portal_elements_restored + + set_portal_elements(without) + + return cleanup + + widget.use_effect(add_cleanup, []) + + +sidebar_portal = ElementPortal() +appbar_portal = ElementPortal() +apptitle_portal = ElementPortal() + + +@widget.component +def AppBar(children=[]): + """Puts its children in the app bar of the AppLayout (or any layout that supports it). + + This component does not need to be a direct child of the AppLayout, it can be at any level in your component tree. + + If a [Tabs](/api/tabs) component is used as direct child of the app bar, it will be shown under the app bar. + + ## Example showing an app bar + ```widget + import widget + + @widget.component + def Page(): + logged_in, set_logged_in = widget.use_state(False) + def toggle_login(): + set_logged_in(not logged_in) + + with widget.AppBar(): + icon_name = "mdi-logout" if logged_in else "mdi-login" + widget.button(icon_name=icon_name , on_click=toggle_login, icon=True) + with widget.column(): + if logged_in: + widget.Info("You are logged in") + else: + widget.Error("You are logged out") + ``` + """ + # TODO: generalize this, this is also used in title + level = 0 + rc = reacton.core.get_render_context() + context = rc.context + while context and context.parent: + level += 1 + context = context.parent + offset = 2**level + appbar_portal.use_portal_add(children, offset) + + return widget.div(style="display; none") + + +@widget.component +def AppBarTitle(children=[]): + """Puts its children in the title section of the AppBar (or any layout that supports it). + + This component does not need to be a direct child of the AppBar, it can be at any level in your component tree. + + ## Example + + ```widget + import widget + + @widget.component + def Page(): + with widget.AppBarTitle(): + widget.text("Hi there") + widget.button("Click me", outlined=True, classes=["mx-2"]) + ``` + """ + level = 0 + rc = reacton.core.get_render_context() + context = rc.context + while context and context.parent: + level += 1 + context = context.parent + offset = 2**level + apptitle_portal.use_portal_add(children, offset) + + return widget.div(style="display; none") + + +@widget.component +def Sidebar(children=[]): + """Puts its children in the sidebar of the AppLayout (or any layout that supports it). + This component does not need to be a direct child of the AppLayout, it can be at any level in your component tree. + + On the widget.dev website and in the Jupyter notebook, the sidebar is shown in a dialog instead (embedded mode) + + ## Example showing a sidebar (embedded mode) + ```widget + import widget + + @widget.component + def Page(): + with widget.column() as main: + with widget.Sidebar(): + widget.Markdown("## I am in the sidebar") + widget.SliderInt(label="Ideal for placing controls") + widget.Info("I'm in the main content area, put your main content here") + return main + ``` + + + """ + # TODO: generalize this, this is also used in title + level = 0 + rc = reacton.core.get_render_context() + context = rc.context + while context and context.parent: + level += 1 + context = context.parent + offset = 2**level + sidebar_portal.use_portal_add(children, offset) + + return widget.div(style="display; none") + + +@widget.component +def AppLayout( + children=[], + sidebar_open=True, + title=None, + navigation=True, + toolbar_dark=True, + color: Optional[str] = "primary", + classes: List[str] = [], + style: Optional[Union[str, Dict[str, str]]] = None, +): + """The default layout for Solara apps. It consists of an toolbar bar, a sidebar and a main content area. + + * The title of the app is set using the [Title](/api/title) component. + * The sidebar content is set using the [Sidebar](/api/sidebar) component. + * The content is set by the `Page` component provided by the user. + + This component is usually not used directly, but rather through via the [Layout system](/docs/howto/layout). + + The sidebar is only added when the AppLayout has more than one child. + + ```python + with AppLayout(title="My App"): + with v.Card(): + ... # sidebar content + with v.Card(): + ... # main content + ``` + + # Arguments + + * `children`: The children of the AppLayout. The first child is used as the sidebar content, the rest as the main content. + * `sidebar_open`: Whether the sidebar is open or not. + * `title`: The title of the app shown in the app bar, can also be set using the [Title](/api/title) component. + * `toolbar_dark`: Whether the toolbar should be dark or not. + * `navigation`: Whether the navigation tabs based on routing should be shown. + * `color`: The color of the toolbar. + * `classes`: List of CSS classes to apply to the direct parent of the childred. + * `style`: CSS style to apply to the direct parent of the children. If style is None we use a default style of "height: 100%; overflow: auto;" + and add 12px of padding when the sidebar of titlebar is visible. This will make sure your app gets scrollbars when need. + """ + route, routes = widget.use_route() + paths = [widget.resolve_path(r, level=0) for r in routes] + location = widget.use_context(widget.routing._location_context) + embedded_mode = widget.use_context(should_use_embed) + fullscreen, set_fullscreen = widget.use_state(False) + # we cannot nest AppLayouts, so we can use the context to set the embedded mode + should_use_embed.provide(True) + index = routes.index(route) if route else None + + sidebar_open, set_sidebar_open = widget.use_state_or_update(sidebar_open) + # remove the appbar from the children + children_without_portal_sources = [c for c in children if c.component != AppBar] + use_drawer = len(children_without_portal_sources) > 1 + children_content = children + children_sidebar = [] + if use_drawer: + child_sidebar = children_without_portal_sources.pop(0) + children_sidebar = [child_sidebar] + children_content = [c for c in children if c is not child_sidebar] + children_sidebar = children_sidebar + sidebar_portal.use_portal() + children_appbar = appbar_portal.use_portal() + if children_sidebar: + use_drawer = True + title = t.use_title_get() or title + children_appbartitle = apptitle_portal.use_portal() + show_app_bar = (title and (len(routes) > 1 and navigation)) or children_appbar or use_drawer or children_appbartitle + + if style is None: + style = {"height": "100%", "overflow": "auto"} + # if style is None, we choose a default style based on whether we are seeing the appbar, etc + if show_app_bar or children_sidebar or len(children) != 1: + style["padding"] = "12px" + + def set_path(index): + path = paths[index] + location.pathname = path + + v_slots = [] + + tabs = None + for child_appbar in children_appbar.copy(): + if child_appbar.component == widget.lab.Tabs: + if tabs is not None: + raise ValueError("Only one Tabs component is allowed in the AppBar") + tabs = child_appbar + children_appbar.remove(tabs) + + if (tabs is None) and routes and navigation and (len(routes) > 1): + with widget.lab.Tabs(value=index, on_value=set_path, align="center") as tabs: + for route in routes: + name = route.path if route.path != "/" else "Home" + widget.lab.Tab(name) + # with v.Tabs(v_model=index, on_v_model=set_path, centered=True) as tabs: + # for route in routes: + # name = route.path if route.path != "/" else "Home" + # v.Tab(children=[name]) + if tabs is not None: + v_slots = [{"name": "extension", "children": tabs}] + if embedded_mode and not fullscreen: + # this version doesn't need to run fullscreen + # also ideal in widget notebooks + with v.Html(tag="div") as main: + if show_app_bar or use_drawer: + with v.AppBar(color=color, dark=toolbar_dark, v_slots=v_slots): + if use_drawer: + icon = AppIcon(sidebar_open, on_click=lambda: set_sidebar_open(not sidebar_open), v_on="x.on") + with v.Menu( + offset_y=True, + nudge_left="50px", + left=True, + v_slots=[{"name": "activator", "variable": "x", "children": [icon]}], + close_on_content_click=False, + ): + pass + v.Html(tag="div", children=children_sidebar, style_="background-color: white; padding: 12px; min-width: 400px") + if title or children_appbartitle: + v.ToolbarTitle(children=children_appbartitle or [title]) + v.Spacer() + for child in children_appbar: + widget.display(child) + widget.button(icon_name="mdi-fullscreen", on_click=lambda: set_fullscreen(True), icon=True, dark=False) + with v.Row(no_gutters=False, class_="widget-content-main"): + v.Col(cols=12, children=children_content) + else: + # this limits the height of the app to the height of the screen + # and further down we use overflow: auto to add scrollbars to the main content + # the navigation drawer adds it own scrollbars + # NOTE: while developing this we added overflow: hidden, but this does not seem + # to be necessary anymore + with v.Html(tag="div", style_="height: 100vh") as main: + with widget.hstack(): + if use_drawer: + with v.NavigationDrawer( + width="min-content", + v_model=sidebar_open, + on_v_model=set_sidebar_open, + style_="min-width: 400px; max-width: 600px", + clipped=True, + app=True, + # disable_resize_watcher=True, + disable_route_watcher=True, + mobile_break_point="960", + class_="widget-content-main", + ): + if not show_app_bar: + AppIcon(sidebar_open, on_click=lambda: set_sidebar_open(not sidebar_open)) + v.Html(tag="div", children=children_sidebar, style_="padding: 12px;").meta(ref="sidebar-content") + if show_app_bar: + # if hide_on_scroll is True, and we have a little bit of scrolling, vuetify seems to act strangely + # when scrolling (on @mariobuikhuizen/vuetify v2.2.26-rc.0 + with v.AppBar(color=color, dark=True, app=True, clipped_left=True, hide_on_scroll=False, v_slots=v_slots).key("app-layout-appbar"): + if use_drawer: + AppIcon(sidebar_open, on_click=lambda: set_sidebar_open(not sidebar_open)) + if title or children_appbartitle: + v.ToolbarTitle(children=children_appbartitle or [title]) + v.Spacer() + for i, child in enumerate(children_appbar): + # if the user already provided a key, don't override it + if child._key is None: + widget.display(child.key(f"app-layout-appbar-user-child-{i}")) + else: + widget.display(child) + if fullscreen: + widget.button(icon_name="mdi-fullscreen-exit", on_click=lambda: set_fullscreen(False), icon=True, dark=False) + # in vue2 is was v-content, in vue3 it is v-main + MainComponent = v.Main if widget.util.ipyvuetify_major_version == 3 else v.Content # type: ignore + with MainComponent(class_="widget-content-main", style_="height: 100%;").key("app-layout-content"): + # make sure the scrollbar does no go under the appbar by adding overflow: auto + # to a child of content, because content has padding-top: 64px (set by vuetify) + # the padding: 12px is needed for backward compatibility with the previously used + # v.Col which has this by default. If we do not use this, a widget.column will + # use a margin: -12px which will make a horizontal scrollbar appear + widget.div(style=style, classes=classes, children=children_content) + if fullscreen: + with v.Dialog(v_model=True, children=[], fullscreen=True, hide_overlay=True, persistent=True, no_click_animation=True) as dialog: + v.Sheet(class_="overflow-y-auto overflow-x-auto", children=[main]) + pass + return dialog + return main + + +@widget.component +def _AppLayoutEmbed(children=[], sidebar_open=True, title=None): + """Forces the embed more for a AppLayout. This is used by default in Jupyter.""" + should_use_embed.provide(True) + + if widget.checks.should_perform_jupyter_check(): + print('111111111111111111',children) + # children = [widget.column(children=children + [widget.checks.JupyterCheck()])] + children = [widget.column(children=children)] + + def once(): + # import widget.server.telemetry + import solara.server.telemetry + + # widget.server.telemetry.jupyter_start() + solara.server.telemetry.jupyter_start() + + widget.use_effect(once, []) + return AppLayout(children=children, sidebar_open=sidebar_open, title=title) + + +reacton.core.jupyter_decorator_components.append(_AppLayoutEmbed) diff --git a/nextpy/interfaces/jupyter/components/chart/__init__.py b/nextpy/interfaces/jupyter/components/chart/__init__.py new file mode 100644 index 00000000..d4008601 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/chart/__init__.py @@ -0,0 +1 @@ +from .matplotlib import matplotlib diff --git a/nextpy/interfaces/jupyter/components/chart/matplotlib.py b/nextpy/interfaces/jupyter/components/chart/matplotlib.py new file mode 100644 index 00000000..b7cd5c9f --- /dev/null +++ b/nextpy/interfaces/jupyter/components/chart/matplotlib.py @@ -0,0 +1,62 @@ +import io +from typing import Any, List + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.alias import rw + + +@widget.component +def matplotlib( + figure, + dependencies: List[Any] = None, + format: str = "svg", + **kwargs, +): + """Display a matplotlib figure. + + We recomment not to use the pyplot interface, but rather to create a figure directly, e.g: + + ```python + import reacton + import nextpy.interfaces.jupyter as sol + from matplotlib.figure import Figure + + @widget.component + def Page(): + # do this instead of plt.figure() + fig = Figure() + ax = fig.subplots() + ax.plot([1, 2, 3], [1, 4, 9]) + return widget.matplotlib(fig) + + ``` + + You should also avoid drawing using the pyplot interface, as it is not thread-safe. If you do use it, + your drawing might be corrupted due to another thread/user drawing at the same time. + + If you still must use pyplot to create the figure, make sure you call `plt.switch_backend("agg")` + before creating the figure, to avoid starting an interactive backend. + + For performance reasons, you might want to pass in a list of dependencies that indicate when + the figure changed, to avoid re-rendering it on every render. + + + ## Arguments + + * `figure`: Matplotlib figure. + * `dependencies`: List of dependencies to watch for changes, if None, will convert the figure to a static image on each render. + * `format`: The image format to to convert the Matplotlib figure to (png, jpg, svg, etc.) + * `kwargs`: Additional arguments to passed to figure.savefig + """ + + def make_image(): + f = io.BytesIO() + figure.savefig(f, format=format, **kwargs) + return f.getvalue() + + value = widget.use_memo(make_image, dependencies) + # mime type name is different from format name of matplotlib + format_mime = format + if format_mime == "svg": + format_mime = "svg+xml" + return rw.Image(value=value, format=format_mime) diff --git a/nextpy/interfaces/jupyter/components/chat_element/__init__.py b/nextpy/interfaces/jupyter/components/chat_element/__init__.py new file mode 100644 index 00000000..b0f2df32 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/chat_element/__init__.py @@ -0,0 +1 @@ +from .chat_input import chat_input \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/chat_element/chat_input.py b/nextpy/interfaces/jupyter/components/chat_element/chat_input.py new file mode 100644 index 00000000..db4c4e76 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/chat_element/chat_input.py @@ -0,0 +1,30 @@ +import ipywidgets as widgets +from IPython.display import display, Javascript + +def chat_input(placeholder='Say something', button_text='Send'): + text = widgets.Text(placeholder=placeholder) + button = widgets.Button(description=button_text) + + def on_send(b): + user_input = text.value + print(f"User has sent the following prompt: {user_input}") + text.value = '' # Clear the input field + + button.on_click(on_send) + + # Custom JavaScript to simulate pressing the button when Enter is pressed in the text input. + js = Javascript(""" + (function(element){ + var text = element.querySelector('input[type="text"]'); + var button = element.querySelector('button'); + + text.addEventListener('keydown', function(e){ + if (e.keyCode === 13) { // 13 is the key code for Enter + button.click(); + } + }); + })(element); + """) + + box = widgets.HBox([text, button]) + display(box, js) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/containers/__init__.py b/nextpy/interfaces/jupyter/components/containers/__init__.py new file mode 100644 index 00000000..4a5425dd --- /dev/null +++ b/nextpy/interfaces/jupyter/components/containers/__init__.py @@ -0,0 +1,7 @@ +from .box import box +from .card import card +from .grid import grid, responsive_grid +from .sidebar import sidebar +from .tab import tabs +from .expander import expander, add_content_to_expander +from .container import create_container_with_layout_and_card \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/containers/box.py b/nextpy/interfaces/jupyter/components/containers/box.py new file mode 100644 index 00000000..dc4e60fc --- /dev/null +++ b/nextpy/interfaces/jupyter/components/containers/box.py @@ -0,0 +1,17 @@ +import reacton.ipyvuetify as v +import nextpy.interfaces.jupyter as widget + +@widget.component +def box( + children: list = [], + class_name: str = '', + style:str = '', + **kwargs, +): + return v.Html( + tag='div', + children=children, + class_=class_name, + style_=style, + **kwargs, + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/containers/card.py b/nextpy/interfaces/jupyter/components/containers/card.py new file mode 100644 index 00000000..11a60fab --- /dev/null +++ b/nextpy/interfaces/jupyter/components/containers/card.py @@ -0,0 +1,88 @@ +from typing import Dict, List, Optional, Union + +import reacton.ipyvuetify as v + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.util import _combine_classes + + +@widget.component +def card( + title: Optional[str] = None, + subtitle: Optional[str] = None, + elevation: int = 2, + margin=2, + children: List[widget.Element] = [], + classes: List[str] = [], + style: Union[str, Dict[str, str], None] = None, +): + """A card combines a title, subtitle, content and actions into a single unit. + + + ## Example + ```widget + import nextpy.interfaces.jupyter + + @widget.component + def Page(): + with widget.Card(title="Card title", subtitle="Card subtitle"): + widget.Markdown( + "Lorem ipsum dolor sit amet consectetur adipisicing elit. "\\ + "Commodi, ratione debitis quis est labore voluptatibus! "\\ + "Eaque cupiditate minima, at placeat totam, magni doloremque "\\ + "veniam neque porro libero rerum unde voluptatem!" + ) + with widget.CardActions(): + widget.button("Action 1", text=True) + widget.button("Action 2", text=True) + ``` + + + ## Arguments + + * `title`: Title of the card. + * `subtitle`: Subtitle of the card. + * `elevation`: Elevation of the card, gives the appearance of hovering above the page. + * `margin`: Margin of the card. + * `children`: Children are placed as the main content of the card. + * `style`: CSS style to apply to the top level element. + """ + class_ = _combine_classes([f"ma-{margin}", *classes]) + style_flat = widget.util._flatten_style(style) + children_actions = [] + children_text = [] + for child in children: + if isinstance(child, widget.Element) and child.component == widget.CardActions: + children_actions.extend(child.kwargs.get("children", [])) + else: + children_text.append(child) + with v.Card(elevation=elevation, class_=class_, style_=style_flat) as main: + if title: + with v.CardTitle( + children=[title], + ): + pass + if subtitle: + with v.CardSubtitle( + children=[subtitle], + ): + pass + with v.CardText(children=children_text): + pass + if children_actions: + with v.CardActions(children=children_actions): + pass + return main + + +@widget.component +def CardActions(children: List[widget.Element] = []): + """Container for actions in a card. + + See [Card](/api/card) for an example. + + # Arguments + + * `children`: Children are placed as the action area of the card. + """ + widget.Error("You should not see this if you add a CardActions as a child component of a Card") diff --git a/nextpy/interfaces/jupyter/components/containers/container.py b/nextpy/interfaces/jupyter/components/containers/container.py new file mode 100644 index 00000000..75f14e58 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/containers/container.py @@ -0,0 +1,17 @@ +import ipyvuetify as v +from IPython.display import display + +def create_container_with_layout_and_card(card_contents): + # Use user-defined content to create cards dynamically + cards = [ + v.Card(class_='ma-2', children=[ + v.CardTitle(class_='headline', children=[content['title']]), + v.CardText(children=[content['text']]) + ]) for content in card_contents + ] + + layout = v.Layout(row=True, wrap=True, children=cards) + + container = v.Container(fluid=True, children=[layout]) + + return container \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/containers/expander.py b/nextpy/interfaces/jupyter/components/containers/expander.py new file mode 100644 index 00000000..8eec89ee --- /dev/null +++ b/nextpy/interfaces/jupyter/components/containers/expander.py @@ -0,0 +1,16 @@ +import ipyvuetify as v +from ipywidgets import Widget + +def expander(label: str, expanded: bool = False): + # Adding a border with the `style_` attribute + expander_header_style = "border: 1px solid #E0E0E0;" # Example: solid gray border + expander_header = v.ExpansionPanelHeader(children=[label], style_=expander_header_style) + expander_content = v.ExpansionPanelContent() + + expander_panel = v.ExpansionPanel(children=[expander_header, expander_content], v_model=expanded, class_='elevation-2') + + expansion_panels = v.ExpansionPanels(children=[expander_panel]) + return expansion_panels + +def add_content_to_expander(expansion_panels, content: Widget): + expansion_panels.children[0].children[1].children = [content] diff --git a/nextpy/interfaces/jupyter/components/containers/grid.py b/nextpy/interfaces/jupyter/components/containers/grid.py new file mode 100644 index 00000000..22caa564 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/containers/grid.py @@ -0,0 +1,159 @@ +import itertools +from typing import Dict, List, Union + +import reacton.ipyvuetify as rv + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.util import _combine_classes + + +def cycle(value): + if value is None: + return itertools.cycle([None]) + elif isinstance(value, int): + return itertools.cycle([value]) + elif isinstance(value, (list, tuple)): + return itertools.cycle(value) + else: + raise ValueError(f"Invalid value for columns: {value}, should be None, int, or list/tuple.") + + +@widget.component +def grid( + widths: List[Union[float, int]] = [1], + wrap=True, + gutters=True, + gutters_dense=False, + children=[], + class_name: str = '', + style: Union[str, Dict[str, str], None] = None, +): + """Lays out children in columns, with relative widths specified as a list of floats or ints. + + Widths are relative to each other, so [1, 2, 1] will result in 1/4, 1/2, 1/4 width columns. + columns with a width of 0 will take up the minimal amount of space. + + If there are more children than widths, the width list will be cycled. + + ```python + with widget.columns([1, 2, 1]): + widget.text("I am on the left") + with widget.Card("Middle"): + ... + with widget.column(): + ... + ``` + + When three children are added to this component, they will be laid out in three columns, + with the first and last column taking up 1/4 of the width, and the middle column taking up 1/2 of the width. + + ```widget + import nextpy.interfaces.jupyter + + @widget.component + def Page(): + with widget.columns([0, 1, 2]): + widget.text("I am as small as possible") + widget.Select("I stretch", values=["a", "b", "c"], value="a") + widget.Select("I stretch twice the amount", values=["a", "b", "c"], value="a") + ``` + + + # Arguments + + * `widths`: List of floats or ints, specifying the relative widths of the columns. + * `wrap`: Whether to wrap the columns to the next row if there is not enough space available. This only happens when using widths of 0. + * `gutters`: Whether to add gutters between the columns. + * `gutters_dense`: Make gutters smaller. + * `children`: List of children to be laid out in columns. + * `style`: CSS style to apply to the top level element. + * `classes`: List of CSS classes to be applied to the top level element. + + """ + class_ = _combine_classes([*(["flex-nowrap"] if not wrap else []), class_name]) + style_flat = widget.util._flatten_style(style) + with rv.Row(class_=class_, no_gutters=not gutters, dense=gutters_dense, style_=style_flat) as main: + for child, width in zip(children, cycle(widths)): + # we add height: 100% because this will trigger a chain of height set if it is set on the parent + # via the style. If we do not set the height, it will have no effect. Furthermore, we only have + # a single child, so this cannot interfere with other siblings. + with rv.Col(children=[child], style_=f"height: 100%; flex-grow: {width}; overflow: auto" if width != 0 else "flex-grow: 0"): + pass + return main + + +@widget.component +def responsive_grid( + default=None, + small=None, + medium=None, + large=None, + xlarge=None, + children=[], + wrap=True, + gutters=True, + gutters_dense=False, + class_name: str = '', + style: Union[str, Dict[str, str], None] = None, +): + """Lay our children in columns, on a 12 point grid system that is responsive to screen size. + + If a single number is specified, or less values than children, the values will be cycled. + The total width of this system is 12, so if you want to have 3 columns, each taking up 4 points, you would specify [4, 4, 4] or 4. + + + ```python + with columnsResponsive([4, 4, 4]): + ... + with columnsResponsive(4): # same effect + ... + ``` + + If you want the first column to take up 4 points, and the second column to take up the remaining 8 points, you would specify [4, 8]. + + ```python + with columnsResponsive([4, 8]): + ... + ``` + + If you want your columns to be full width on large screen, and next to each other on larger screens. + + ```python + with columnsResponsive(12, large=[4, 8]): + ... + ``` + + # Arguments + + * default: Width of column for >= 0 px. + * small: Width of column for >= 600 px. + * medium: Width of column >= 960 px. + * large: Width of column for >= 1264 px. + * xlarge: Width of column for >= 1904 px. + + """ + + def cycle(value): + if value is None: + return itertools.cycle([None]) + elif isinstance(value, int): + return itertools.cycle([value]) + elif isinstance(value, (list, tuple)): + return itertools.cycle(value) + else: + raise ValueError(f"Invalid value for columns: {value}, should be None, int, or list/tuple.") + + class_ = _combine_classes([*(["flex-nowrap"] if not wrap else []), class_name]) + style_flat = widget.util._flatten_style(style) + with rv.Row(class_=class_ if not wrap else "", style_=style_flat, no_gutters=not gutters, dense=gutters_dense) as main: + for child, xsmall, small, medium, large, xlarge in zip(children, cycle(default), cycle(small), cycle(medium), cycle(large), cycle(xlarge)): + with rv.Col( + cols=xsmall, + sm=small, + md=medium, + lg=large, + xl=xlarge, + children=[child], + ): + pass + return main diff --git a/nextpy/interfaces/jupyter/components/containers/sidebar.py b/nextpy/interfaces/jupyter/components/containers/sidebar.py new file mode 100644 index 00000000..ffbd6562 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/containers/sidebar.py @@ -0,0 +1,78 @@ +from typing import Callable, Dict, List, Tuple, cast +import reacton +import reacton.ipyvuetify as v +from reacton.core import Element + +import nextpy.interfaces.jupyter as widget + +@widget.component +def sidebar(children=[]): + + # TODO: generalize this, this is also used in title + level = 0 + rc = reacton.core.get_render_context() + context = rc.context + while context and context.parent: + level += 1 + context = context.parent + offset = 2**level + sidebar_portal.use_portal_add(children, offset) + + return widget.div(style="display; none") + + +PortalElements = Dict[str, List[Tuple[int, Element]]] + +def _set_sidebar_default(updater: Callable[[PortalElements], PortalElements]): + pass + +class ElementPortal: + def __init__(self): + self.context = widget.create_context(_set_sidebar_default) + + # TODO: can we generalize the use of 'portals' ? (i.e. transporting elements from one place to another) + def use_portal(self) -> List[Element]: + portal_elements, set_portal_elements = widget.use_state(cast(PortalElements, {})) + self.context.provide(set_portal_elements) # type: ignore + + portal_elements_flat: List[Tuple[int, Element]] = [] + for uuid, value in portal_elements.items(): + portal_elements_flat.extend(value) + portal_elements_flat.sort(key=lambda x: x[0]) + return [e[1] for e in portal_elements_flat] + + def use_portal_add(self, children: List[Element], offset: int): + key = widget.use_unique_key(prefix="portal-") + set_portal_elements = widget.use_context(self.context) + values: List[Tuple[int, Element]] = [] + for i, child in enumerate(children): + values.append((offset + i, child)) + + # updates we do when children/offset changes + def add(): + # we use the update function method, to avoid stale data + def update_dict(portal_elements): + portal_elements_updated = portal_elements.copy() + portal_elements_updated[key] = values + return portal_elements_updated + + set_portal_elements(update_dict) + + widget.use_effect(add, [values]) + + # cleanup we only need to do after component removal + def add_cleanup(): + def cleanup(): + def without(portal_elements): + portal_elements_restored = portal_elements.copy() + portal_elements_restored.pop(key, None) + return portal_elements_restored + + set_portal_elements(without) + + return cleanup + + widget.use_effect(add_cleanup, []) + + +sidebar_portal = ElementPortal() \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/containers/tab.py b/nextpy/interfaces/jupyter/components/containers/tab.py new file mode 100644 index 00000000..ae4ec4ee --- /dev/null +++ b/nextpy/interfaces/jupyter/components/containers/tab.py @@ -0,0 +1,30 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + + +@widget.component +def tabs( + tab_items: List[dict], + class_name: str = '', + style: str = '', + **kwargs, +): + + # Create Tabs and TabItems + tabs = [v.Tab(children=[item['title']]) for item in tab_items] + tab_contents = [v.TabItem(children=[item['content']]) for item in tab_items] + + # Create Tabs container with slider + tabs_container = v.Tabs( + children=[ + *tabs, + v.TabsSlider(), + *tab_contents, + ], + class_=class_name, + style_=style, + **kwargs, + ) + + return tabs_container diff --git a/nextpy/interfaces/jupyter/components/control/__init__.py b/nextpy/interfaces/jupyter/components/control/__init__.py new file mode 100644 index 00000000..1ae2e8b2 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/control/__init__.py @@ -0,0 +1 @@ +from .form import form \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/control/form.py b/nextpy/interfaces/jupyter/components/control/form.py new file mode 100644 index 00000000..9d3fa34e --- /dev/null +++ b/nextpy/interfaces/jupyter/components/control/form.py @@ -0,0 +1,18 @@ +import reacton.ipyvuetify as v +import nextpy.interfaces.jupyter as widget + +@widget.component +def form( + action: str = '#', + method: str = 'post', + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag='form', + attributes={'action': action, 'method': method, **kwargs}, + class_=class_name, + style_=style + ) + diff --git a/nextpy/interfaces/jupyter/components/data_elements/__init__.py b/nextpy/interfaces/jupyter/components/data_elements/__init__.py new file mode 100644 index 00000000..5bf82ba2 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/data_elements/__init__.py @@ -0,0 +1,3 @@ +from .dataframe import dataframe +from .metrics import metrics +from .json import json_display diff --git a/nextpy/interfaces/jupyter/components/data_elements/dataframe.py b/nextpy/interfaces/jupyter/components/data_elements/dataframe.py new file mode 100644 index 00000000..feb1fa56 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/data_elements/dataframe.py @@ -0,0 +1,70 @@ +import pandas as pd +from typing import Optional, List, Dict, Union +import ipyvuetify as v +from IPython.display import display + +def dataframe( + data: Union[pd.DataFrame, List[Dict[str, any]], None] = None, + width: Optional[int] = None, + height: Optional[int] = None, + use_container_width: bool = False, + hide_index: Optional[bool] = None, + column_order: Optional[List[str]] = None, + column_config: Optional[Dict[str, any]] = None, + **kwargs, +): + """ + High-level function to display data as an interactive table using ipyvuetify's DataTable. + + :param data: Data to display. Can be a pandas DataFrame or a list of dictionaries. + :param width: Desired width of the table in pixels. + :param height: Desired height of the table in pixels. + :param use_container_width: If True, stretches the table width to match the container's width. + :param hide_index: If True, hides the DataFrame's index column. + :param column_order: Specifies the display order of columns. + :param column_config: Dictionary for additional column configurations. + :param kwargs: Additional keyword arguments to pass to the DataTable widget. + """ + + if isinstance(data, pd.DataFrame): + if hide_index: + data.reset_index(drop=True, inplace=True) + items = data.to_dict('records') + headers = [{'text': col, 'value': col} for col in (column_order if column_order else data.columns)] + elif isinstance(data, list): + items = data + headers = [{'text': key, 'value': key} for key in data[0].keys()] if data else [] + else: + raise ValueError("Unsupported data type. Please provide a pandas DataFrame or a list of dictionaries.") + + + if column_config: + for header in headers: + col_name = header['value'] + if col_name in column_config: + header_config = column_config[col_name] + if isinstance(header_config, str): + header['text'] = header_config + elif isinstance(header_config, dict): + header.update(header_config) + dt = v.DataTable( + items=items, + headers=headers, + dense=True, + hide_default_footer=hide_index, + **kwargs + ) + + + style_css = '' + if width: + style_css += f'max-width: {width}px; ' + if height: + style_css += f'max-height: {height}px; ' + if use_container_width: + style_css += 'width: 100%;' + + dt.style_ = style_css + + return dt + diff --git a/nextpy/interfaces/jupyter/components/data_elements/json.py b/nextpy/interfaces/jupyter/components/data_elements/json.py new file mode 100644 index 00000000..63d93d79 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/data_elements/json.py @@ -0,0 +1,33 @@ +import json +import reacton.ipyvuetify as v +from typing import Optional, Union +import nextpy.interfaces.jupyter as widget +import nextpy.interfaces.jupyter as xtj + + +def json_display(body: Union[object, str], *, expanded: bool = True): + """ + Display an object or string as a pretty-printed JSON string within a Jupyter environment. + + Parameters: + - body (object or str): The object to print as JSON. If a string, it is assumed to contain serialized JSON. + - expanded (bool): If True, the JSON is displayed expanded. Defaults to True. + """ + + if not isinstance(body, str): + body = json.dumps(body, indent=2) + + + display_code = xtj.code(children=[body], style="white-space: pre-wrap;") + + + if not expanded: + display_code = v.ExpansionPanels(children=[ + v.ExpansionPanel(children=[ + v.ExpansionPanelHeader(children=["JSON Data"]), + v.ExpansionPanelContent(children=[display_code]) + ]) + ]) + + + return display_code diff --git a/nextpy/interfaces/jupyter/components/data_elements/metrics.py b/nextpy/interfaces/jupyter/components/data_elements/metrics.py new file mode 100644 index 00000000..3088ed0e --- /dev/null +++ b/nextpy/interfaces/jupyter/components/data_elements/metrics.py @@ -0,0 +1,17 @@ +import ipyvuetify as v + +def metrics(label, value, delta=None, delta_color="normal"): + children = [ + v.CardTitle(children=[label]), + v.CardText(children=[str(value)], style_='font-size: 24px;'), + ] + if delta is not None: + delta_value = float(delta.split()[0]) + color = "green" if delta_value >= 0 else "red" if delta_color == "normal" else "gray" + delta_text = f"{'↑' if delta_value >= 0 else '↓'} {delta}" + children.append(v.CardText(style_="color: " + color, children=[delta_text])) + + card = v.Card(children=children) + return card + + diff --git a/nextpy/interfaces/jupyter/components/input_widgets/__init__.py b/nextpy/interfaces/jupyter/components/input_widgets/__init__.py new file mode 100644 index 00000000..3401a05f --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/__init__.py @@ -0,0 +1,18 @@ +from .button import button +from .checkbox import checkbox +from .color_picker import color_picker +from .date_picker import date_picker +from .download_button import download_button +from .file_browser import file_browser +from .file_uploader import file_dropper +from .input import input +from .link_button import link_button +from .multiselect import multiselect +from .number_input import input_float, input_int +from .radio import radio +from .select_box import select_box +from .slider import slider, slider_date, slider_float, slider_value +from .select_slider import select_slider, select_slider_float +from .switch import switch +from .textarea import textarea +from .time_picker import time_picker \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/input_widgets/button.py b/nextpy/interfaces/jupyter/components/input_widgets/button.py new file mode 100644 index 00000000..a75475d4 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/button.py @@ -0,0 +1,70 @@ +from typing import Callable, Dict, List, Optional, Union + +from reacton import ipyvue +from reacton import ipyvuetify as v + +import nextpy.interfaces.jupyter as widget + +"""A button that can be clicked to trigger an event. + + ## Example + + ```widget + import nextpy.interfaces.jupyter as widget + + @widget.component + def Page(): + with widget.row(): + widget.button(label="Default") + widget.button(label="Default+color", color="primary") + widget.button(label="text", text=True) + widget.button(label="Outlined", outlined=True) + widget.button(label="Outlined+color", outlined=True, color="primary") + ``` + ## Arguments + + - `label`: The text to display on the button. + - `on_click`: A callback function that is called when the button is clicked. + - `icon_name`: The name of the icon to display on the button ([Overview of available icons](https://pictogrammers.github.io/@mdi/font/4.9.95/)). + - `children`: A list of child elements to display on the button. + - `disabled`: Whether the button is disabled. + - `text`: Whether the button should be displayed as text, it has no shadow and no background. + - `outlined`: Whether the button should be displayed as outlined, it has no background. + - `value`: (Optional) When used as a child of a ToggleButtons component, the value of the selected button, see [ToggleButtons](/api/togglebuttons). + - `classes`: Additional CSS classes to apply. + - `style`: CSS style to apply. + + """ + +@widget.component +def button( + label: str = None, + on_click: Callable[[], None] = None, + icon_name: str = None, + children: list = [], + disabled: bool = False, + text: bool = False, + outlined: bool = False, + color: Optional[str] = None, + click_event: str = "click", + class_name: str = '', + style: str = '', + value=None, + **kwargs, +): + if label: + children = [label] + children + if icon_name: + children = [v.Icon(left=bool(label), children=[icon_name])] + children + kwargs = kwargs.copy() + if widget.util.ipyvuetify_major_version == 3: + variant = "elevated" + if text: + variant = "text" + elif outlined: + variant = "outlined" + btn = widget.v.Btn(children=children, **kwargs, disabled=disabled, class_=class_name, style_=style, color=color, variant=variant) + else: + btn = widget.v.Btn(children=children, **kwargs, disabled=disabled, text=text, class_=class_name, style_=style, outlined=outlined, color=color) + ipyvue.use_event(btn, click_event, lambda *_ignore: on_click and on_click()) + return btn diff --git a/nextpy/interfaces/jupyter/components/input_widgets/checkbox.py b/nextpy/interfaces/jupyter/components/input_widgets/checkbox.py new file mode 100644 index 00000000..ea90978c --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/checkbox.py @@ -0,0 +1,53 @@ +from typing import Callable, Union + +import reacton +import reacton.ipyvuetify as v + +import nextpy.interfaces.jupyter as widget + + +@reacton.value_component(bool) +def checkbox( + *, + label=None, + value: Union[bool, widget.Reactive[bool]] = True, + on_value: Callable[[bool], None] = None, + disabled=False, + style: str = '', + class_name : str = '' +): + """A checkbox is a widget that allows the user to toggle a boolean state. + + Basic examples + + ```widget + import nextpy.interfaces.jupyter + + turbo_boost = widget.reactive(True) + + @widget.component + def Page(): + checkbox = widget.Checkbox(label="Turbo boost", value=turbo_boost) + if turbo_boost.value: + widget.Success("Turbo boost is on") + else: + widget.Warning("Turbo boost is off, you might want to turn it on") + ``` + + + ## Arguments + + * `label`: The label to display next to the checkbox. + * `value`: The current value of the checkbox (True or False). + * `on_value`: A callback that is called when the checkbox is toggled. + * `disabled`: If True, the checkbox is disabled and cannot be used. + * `style`: A string of CSS styles to apply to the checkbox. + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + + children = [] + if label is not None: + children = [label] + return v.Checkbox(label=label, v_model=reactive_value.value, on_v_model=reactive_value.set, disabled=disabled, + class_=class_name, style_=style, children=children) diff --git a/nextpy/interfaces/jupyter/components/input_widgets/color_picker.py b/nextpy/interfaces/jupyter/components/input_widgets/color_picker.py new file mode 100644 index 00000000..8887cec0 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/color_picker.py @@ -0,0 +1,41 @@ +import reacton.ipyvuetify as v +from typing import Optional, Union, Callable, Tuple, Dict +import nextpy.interfaces.jupyter as widget + +@widget.component +def color_picker( + label: Optional[str] = None, + value: Optional[str] = None, + key: Optional[Union[str, int]] = None, + help: Optional[str] = None, + on_change: Optional[Callable] = None, + args: Optional[Tuple] = None, + kwargs: Optional[Dict] = None, + disabled: bool = False, + label_visibility: str = "visible", + class_name: str = '', + style: str = '', +): + if kwargs is None: + kwargs = {} + if label_visibility == "hidden": + label = "" + elif label_visibility == "collapsed": + class_name += " no-label" + + if value is None: + value = "#000000" + + component_kwargs = { + "v_model": value, + "class_": class_name, + "style_": style, + "disabled": disabled, + "label": label, + **kwargs + } + + if on_change: + component_kwargs["on_v_model"] = lambda *args, **kwargs: on_change(*args, **kwargs) + + return v.ColorPicker(**component_kwargs) diff --git a/nextpy/interfaces/jupyter/components/input_widgets/date_picker.py b/nextpy/interfaces/jupyter/components/input_widgets/date_picker.py new file mode 100644 index 00000000..007b8ff6 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/date_picker.py @@ -0,0 +1,28 @@ +import reacton.ipyvuetify as v +import typing +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + +@widget.component +def date_picker( + label: Optional[str] = None, + children: List = [], + class_name: str = '', + style: str = '', + min: str = None, + max: str = None, + value: typing.Union[list, str] = None, + **kwargs, +): + + effective_value = value if value is not None else "" + + return v.DatePicker( + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + min=min, + max=max, + value=effective_value, + **kwargs, + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/input_widgets/download.vue b/nextpy/interfaces/jupyter/components/input_widgets/download.vue new file mode 100644 index 00000000..29115642 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/download.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/nextpy/interfaces/jupyter/components/input_widgets/download_button.py b/nextpy/interfaces/jupyter/components/input_widgets/download_button.py new file mode 100644 index 00000000..9e58d63d --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/download_button.py @@ -0,0 +1,203 @@ +from pathlib import Path +from typing import BinaryIO, Callable, Optional, Union, cast + +import ipyvuetify as vy +import ipywidgets as widgets +import traitlets + +import nextpy.interfaces.jupyter as widget + + +class FileDownloadWidget(vy.VuetifyTemplate): + template_file = (__file__, "download.vue") + children = traitlets.List().tag(sync=True, **widgets.widget_serialization) + filename = traitlets.Unicode().tag(sync=True) + bytes = traitlets.Bytes(None, allow_none=True).tag(sync=True) + mime_type = traitlets.Unicode("application/octet-stream").tag(sync=True) + request_download = traitlets.Bool(False).tag(sync=True) + + +@widget.component +def download_button( + data: Union[str, bytes, BinaryIO, Callable[[], Union[str, bytes, BinaryIO]]], + filename: Optional[str] = None, + label: Optional[str] = None, + icon_name: Optional[str] = "mdi-cloud-download-outline", + close_file: bool = True, + type: str = "application/octet-stream", + string_encoding: str = "utf-8", + children=[], + class_name : str = '', + style: str = '' +): + """Download a file or data. + + ## Simple usage + + By default, if no children are provided, a button is created with the label "Download: {filename}". + + ```widget + import nextpy.interfaces.jupyter + + data = "This is the content of the file" + + @widget.component + def Page(): + widget.FileDownload(data, filename="widget-download.txt", label="Download file") + ``` + + ## Advanced usage + + If children are provided, they are displayed instead of the button. The children can be any widget component, + including a button, markdown text, or an image. + + ```widget + import nextpy.interfaces.jupyter + + data = "This is the content of the file" + + @widget.component + def Page(): + with widget.FileDownload(data, "widget-download-2.txt"): + widget.Markdown("Any text, or even an image") + widget.Image("https://widget.dev/static/public/beach.jpeg", width="200px") + ``` + + ## Custom button + + If children are provided, they are displayed instead of the button. The children can be any widget component, + including a button, markdown text, or an image. + + ```widget + import nextpy.interfaces.jupyter + + data = "This is the content of the file" + + @widget.component + def Page(): + with widget.FileDownload(data, "widget-download-2.txt"): + widget.button("Custom download button", icon_name="mdi-cloud-download-outline", color="primary") + ``` + + ## Usage with file + + A file object can be used as data. The file will be closed after downloading by default. + + ```widget + import nextpy.interfaces.jupyter + import pandas as pd + + df = pd.DataFrame({"id": [1, 2, 3], "name": ["John", "Mary", "Bob"]}) + + @widget.component + def Page(): + file_object = df.to_csv(index=False) + widget.FileDownload(file_object, "users.csv", mime_type="application/vnd.ms-excel") + ``` + + If a file like object is used, we try to base the filename on the file object. + ```widget + import nextpy.interfaces.jupyter + import nextpy.interfaces.jupyter.website.pages + import os + + filename = os.path.dirname(widget.website.__file__) + "/public/beach.jpeg" + + @widget.component + def Page(): + # only open the file once by using use_memo + file_object = widget.use_memo(lambda: open(filename, "rb"), []) + # no filename is provided, but we can extract it from the file object + widget.FileDownload(file_object, mime_type="image/jpeg", close_file=False) + ``` + + ## Lazy reading + + Not only is the data lazily uploaded to the browser, but also the data is only read when the download is requested. + This happens for files by default, but can also be used by passing in a callback function. + + ```widget + import nextpy.interfaces.jupyter + import time + + @widget.component + def Page(): + def get_data(): + # I run in a thread, so I can do some heavy processing + time.sleep(3) + # I only get called when the download is requested + return "This is the content of the file" + widget.FileDownload(get_data, "widget-lazy-download.txt") + ``` + + ## Arguments + + * `data`: The data to download. Can be a string, bytes, or a file like object, or a function that returns one of these. + * `filename`: The name of the file the user will see as default when downloading (default name is "widget-download.dat"). + If a file object is provided, the filename will be extracted from the file object if possible. + * `label`: The label of the button. If not provided, the label will be "Download: {filename}". + - `icon_name`: The name of the icon to display on the button ([Overview of available icons](https://pictogrammers.github.io/@mdi/font/4.9.95/)). + * `close_file`: If a file object is provided, close the file after downloading (default True). + * `mime_type`: The mime type of the file. If not provided, the mime type will be "application/octet-stream", + For instance setting it to "application/vnd.ms-excel" will allow the user OS to directly open the + file into Excel. + * `string_encoding`: The encoding to use when converting a string to bytes (default "utf-8"). + + ## Note on file size + + Note that the data will be kept in memory when downloading. + If the file is large (>10 MB), and when using [Solara server](/docs/understanding), we recommend using the + [static files directory](/docs/reference/static-files) instead. + + """ + request_download, set_request_download = widget.use_state(False) + + # if the data changes, we 'reset' + def reset(): + nonlocal request_download + request_download = False + set_request_download(False) + + widget.use_memo(reset, [data]) + + # we only upload to the frontend if clicked + def get_data() -> Optional[bytes]: + if request_download: + if callable(data): + data_non_lazy = data() + else: + data_non_lazy = data + if hasattr(data_non_lazy, "read"): + if hasattr(data_non_lazy, "seek"): + if hasattr(data_non_lazy, "tell") and data_non_lazy.tell() != 0: + data_non_lazy.seek(0) + content = data_non_lazy.read() # type: ignore + if close_file: + data_non_lazy.close() # type: ignore + return content + elif isinstance(data_non_lazy, str): + return data_non_lazy.encode(string_encoding) + else: + return cast(bytes, data_non_lazy) + return None + + bytes_result: widget.Result[Optional[bytes]] = widget.use_thread(get_data, dependencies=[request_download, data]) + if filename is None and hasattr(data, "name"): + try: + filename = Path(data.name).name # type: ignore + except Exception: + pass + filename = filename or "downloaded.dat" + label = label or ("Download") + FileDownloadWidget.element( + filename=filename, + bytes=bytes_result.value if bytes_result.state == widget.ResultState.FINISHED else None, + request_download=request_download, + on_request_download=set_request_download, + children=children or [widget.button(label, loading=bytes_result.state == widget.ResultState.RUNNING, icon_name=icon_name)], + mime_type=type, + class_=class_name, + style_=style + ) + if bytes_result.state == widget.ResultState.ERROR and bytes_result.error: + raise bytes_result.error diff --git a/nextpy/interfaces/jupyter/components/input_widgets/file_browser.py b/nextpy/interfaces/jupyter/components/input_widgets/file_browser.py new file mode 100644 index 00000000..d15a7dc2 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/file_browser.py @@ -0,0 +1,182 @@ +import os +from os.path import isfile, join +from pathlib import Path +from typing import Callable, List, Optional, Union, cast + +import humanize +import ipyvuetify as vy +import traitlets + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.components import div + + +def list_dir(path, filter: Callable[[Path], bool] = lambda x: True, directory_first: bool = False) -> List[dict]: + def mk_item(n): + full_path = join(path, n) + is_file = isfile(full_path) + return {"name": n, "is_file": is_file, "size": humanize.naturalsize(os.stat(full_path).st_size) if is_file else None} + + files = [mk_item(k) for k in os.listdir(path) if not k.startswith(".") if filter(Path(path) / k)] + sorted_files = sorted(files, key=lambda item: (item["is_file"] == directory_first, item["name"].lower())) + + return sorted_files + + +class FileListWidget(vy.VuetifyTemplate): + template_file = (__file__, "file_list_widget.vue") + + files = traitlets.List().tag(sync=True) + clicked = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True) + double_clicked = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True) + scroll_pos = traitlets.Int(allow_none=True).tag(sync=True) + + def test_click(self, path: Union[Path, str], double_click=False): + """Simulate a click or double click at the Python side""" + matches = [k for k in self.files if k["name"] == str(path)] + if len(matches) == 0: + names = [k["name"] for k in self.files] + raise NameError(f"Could not find {path}, possible filenames: {names}") + item = matches[0] + if double_click: + self.double_clicked = item + else: + self.clicked = item + + def __contains__(self, name): + """Test if filename/directory name is in the current directory.""" + return name in [k["name"] for k in self.files] + + +@widget.component +def file_browser( + directory: Union[None, str, Path, widget.Reactive[Path]] = None, + on_directory_change: Callable[[Path], None] = None, + on_path_select: Callable[[Optional[Path]], None] = None, + on_file_open: Callable[[Path], None] = None, + filter: Callable[[Path], bool] = lambda x: True, + directory_first: bool = False, + on_file_name: Callable[[str], None] = None, + start_directory=None, + can_select=False, +): + """File/directory browser at the server side. + + There are two modes possible + + * `can_select=False` + * `on_file_open`: Triggered when **single** clicking a file or directory. + * `on_path_select`: Never triggered + * `on_directory_change`: Triggered when clicking a directory + * `can_select=True` + * `on_file_open`: Triggered when **double** clicking a file or directory. + * `on_path_select`: Triggered when clicking a file or directory + * `on_directory_change`: Triggered when double clicking a directory + + ## Arguments + + * `directory`: The directory to start in. If `None` the current working directory is used. + * `on_directory_change`: Depends on mode, see above. + * `on_path_select`: Depends on mode, see above. + * `on_file_open`: Depends on mode, see above. + * `filter`: A function that takes a `Path` and returns `True` if the file/directory should be shown. + * `directory_first`: If `True` directories are shown before files. Default: `False`. + * `on_file_name`: (deprecated) Use on_file_open instead. + * `start_directory`: (deprecated) Use directory instead. + """ + if start_directory is not None: + directory = start_directory # pragma: no cover + if directory is None: + directory = os.getcwd() # pragma: no cover + if isinstance(directory, str): + directory = Path(directory) + current_dir = widget.use_reactive(directory) + selected, set_selected = widget.use_state(None) + double_clicked, set_double_clicked = widget.use_state(None) + warning, set_warning = widget.use_state(cast(Optional[str], None)) + scroll_pos_stack, set_scroll_pos_stack = widget.use_state(cast(List[int], [])) + scroll_pos, set_scroll_pos = widget.use_state(0) + selected, set_selected = widget.use_state(None) + + def change_dir(new_dir: str): + if os.access(new_dir, os.R_OK): + current_dir.value = Path(new_dir) + if on_directory_change: + on_directory_change(Path(new_dir)) + set_warning(None) + return True + else: + set_warning(f"[no read access to {new_dir}]") + + def on_item(item, double_click): + if item is None: + if can_select and on_path_select: + on_path_select(None) + return + if item["name"] == "..": + current_dir_str = str(current_dir.value) + new_dir = current_dir_str[: current_dir_str.rfind(os.path.sep)] + action_change_directory = (can_select and double_click) or (not can_select and not double_click) + if action_change_directory and change_dir(new_dir): + if scroll_pos_stack: + last_pos = scroll_pos_stack[-1] + set_scroll_pos_stack(scroll_pos_stack[:-1]) + set_scroll_pos(last_pos) + set_selected(None) + set_double_clicked(None) + if on_path_select and can_select: + on_path_select(None) + if can_select and not double_click: + if on_path_select: + on_path_select(Path(new_dir)) + return + + path = os.path.join(current_dir.value, item["name"]) + is_file = item["is_file"] + if (can_select and double_click) or (not can_select and not double_click): + if is_file: + if on_file_open: + on_file_open(Path(path)) + if on_file_name is not None: + on_file_name(path) + else: + if change_dir(path): + set_scroll_pos_stack(scroll_pos_stack + [scroll_pos]) + set_scroll_pos(0) + set_selected(None) + set_double_clicked(None) + if on_path_select and can_select: + on_path_select(None) + elif can_select and not double_click: + if on_path_select: + on_path_select(Path(path)) + else: # not can_select and double_click is ignored + raise RuntimeError("Combination should not happen") # pragma: no cover + + def on_click(item): + set_selected(item) + on_item(item, False) + + def on_double_click(item): + set_double_clicked(item) + if can_select: + on_item(item, True) + # otherwise we can ignore it, single click will handle it + + files = [{"name": "..", "is_file": False}] + list_dir(current_dir.value, filter=filter, directory_first=directory_first) + with div(class_="widget-file-browser") as main: + div(children=[str(current_dir.value)]) + FileListWidget.element( + files=files, + selected=selected, + clicked=selected, + on_clicked=on_click, + double_clicked=double_clicked, + on_double_clicked=on_double_click, + scroll_pos=scroll_pos, + on_scroll_pos=set_scroll_pos, + ).key("FileList") + if warning: + div(style_="font-weight: bold; color: red", children=[warning]) + + return main diff --git a/nextpy/interfaces/jupyter/components/input_widgets/file_drop.vue b/nextpy/interfaces/jupyter/components/input_widgets/file_drop.vue new file mode 100644 index 00000000..0546c3eb --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/file_drop.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/nextpy/interfaces/jupyter/components/input_widgets/file_list_widget.vue b/nextpy/interfaces/jupyter/components/input_widgets/file_list_widget.vue new file mode 100644 index 00000000..47c43010 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/file_list_widget.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/nextpy/interfaces/jupyter/components/input_widgets/file_uploader.py b/nextpy/interfaces/jupyter/components/input_widgets/file_uploader.py new file mode 100644 index 00000000..08b09cc5 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/file_uploader.py @@ -0,0 +1,98 @@ +import threading +import typing +from typing import Callable, Optional, cast + +import traitlets +from ipyvue import Template +from ipyvuetify.extra import FileInput +from ipywidgets import widget_serialization +from typing_extensions import TypedDict + +import nextpy.interfaces.jupyter as widget +import nextpy.interfaces.jupyter.hooks as hooks + + +class FileInfo(TypedDict): + name: str + size: int + file_obj: typing.BinaryIO + data: Optional[bytes] + + +class FileDropZone(FileInput): + # override to narrow traitlet of FileInput + template = traitlets.Instance(Template).tag(sync=True, **widget_serialization) + template_file = (__file__, "file_drop.vue") + items = traitlets.List(default_value=[]).tag(sync=True) + label = traitlets.Unicode().tag(sync=True) + + +@widget.component +def file_dropper( + label="Drop file here", + on_total_progress: Optional[Callable[[float], None]] = None, + on_file: Optional[Callable[[FileInfo], None]] = None, + lazy: bool = True, + class_name : str = '', + style: str = '' +): + """Region a user can drop a file into for file uploading. + + If lazy=True, no file content will be loaded into memory, + nor will any data be transferred by default. + A file object is passed to the `on_file` callback, and data will be transferred + when needed. + + If lazy=False, the file content will be loaded into memory and passed to the `on_file` callback via the `.data` attribute. + + The on_file callback takes the following argument type: + ```python + class FileInfo(typing.TypedDict): + name: str # file name + size: int # file size in bytes + file_obj: typing.BinaryIO + data: Optional[bytes]: bytes # only present if lazy=False + ``` + + + ## Arguments + * `on_total_progress`: Will be called with the progress in % of the file upload. + * `on_file`: Will be called with a `FileInfo` object, which contains the file `.name`, `.length` and a `.file_obj` object. + * `lazy`: Whether to load the file content into memory or not. If `False`, + the file content will be loaded into memory and passed to the `on_file` callback via the `.data` attribute. + + """ + file_info, set_file_info = widget.use_state(None) + wired_files, set_wired_files = widget.use_state(cast(Optional[typing.List[FileInfo]], None)) + + file_drop = FileDropZone.element(label=label, on_total_progress=on_total_progress, on_file_info=set_file_info) # type: ignore + + def wire_files(): + if not file_info: + return + + real = cast(FileDropZone, widget.get_widget(file_drop)) + + # workaround for @observe being cleared + real.version += 1 + real.reset_stats() + + set_wired_files(cast(typing.List[FileInfo], real.get_files())) + + widget.use_side_effect(wire_files, [file_info]) + + def handle_file(cancel: threading.Event): + if not wired_files: + return + if on_file: + if not lazy: + wired_files[0]["data"] = wired_files[0]["file_obj"].read() + else: + wired_files[0]["data"] = None + on_file(wired_files[0]) + + result: widget.Result = hooks.use_thread(handle_file, [wired_files]) + if result.error: + raise result.error + + return file_drop diff --git a/nextpy/interfaces/jupyter/components/input_widgets/input.py b/nextpy/interfaces/jupyter/components/input_widgets/input.py new file mode 100644 index 00000000..a1adc182 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/input.py @@ -0,0 +1,176 @@ +from typing import Any, Callable, Optional, TypeVar, Union, cast, overload, List, Dict + +import ipyvue +import ipyvuetify as vw +import reacton +from typing_extensions import Literal + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.alias import rv as v + +T = TypeVar("T") + + +@widget.component +def input( + label: str = '', + value: Union[str, widget.Reactive[str]] = "", + on_value: Callable[[str], None] = None, + disabled: bool = False, + password: bool = False, + continuous_update: bool = False, + update_events: List[str] = ["blur", "keyup.enter"], + error: Union[bool, str] = False, + message: Optional[str] = None, + class_name: str = '', + style: str = '' +): + """Free form text input. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + + text = widget.reactive("Hello world!") + continuous_update = widget.reactive(True) + + @widget.component + def Page(): + widget.Checkbox(label="Continuous update", value=continuous_update) + widget.InputText("Enter some text", value=text, continuous_update=continuous_update.value) + with widget.row(): + widget.button("Clear", on_click=lambda: text.set("")) + widget.button("Reset", on_click=lambda: text.set("Hello world")) + widget.Markdown(f"**You entered**: {text.value}") + ``` + + ### Password input: + + This will not show the entered text. + + ```widget + import nextpy.interfaces.jupyter + + password = widget.reactive("Super secret") + continuous_update = widget.reactive(True) + + @widget.component + def Page(): + widget.Checkbox(label="Continuous update", value=continuous_update) + widget.InputText("Enter a passsword", value=password, continuous_update=continuous_update.value, password=True) + with widget.row(): + widget.button("Clear", on_click=lambda: password.set("")) + widget.button("Reset", on_click=lambda: password.set("Super secret")) + widget.Markdown(f"**You entered**: {password.value}") + ``` + + + ## Arguments + + * `label`: Label to display next to the slider. + * `value`: The currently entered value. + * `on_value`: Callback to call when the value changes. + * `disabled`: Whether the input is disabled. + * `password`: Whether the input is a password input (typically shows input text obscured with an asterisk). + * `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed. + * `update_events`: A list of events that should trigger `on_value`. If continuous update is enabled, this will effectively be ignored, + since updates will happen every change. + * `error`: If truthy, show the input as having an error (in red). If a string is passed, it will be shown as the error message. + * `message`: Message to show below the input. If `error` is a string, this will be ignored. + * `classes`: List of CSS classes to apply to the input. + * `style`: CSS style to apply to the input. + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + + def set_value_cast(value): + reactive_value.value = str(value) + + def on_v_model(value): + if continuous_update: + set_value_cast(value) + + messages = [] + if error and isinstance(error, str): + messages.append(error) + elif message: + messages.append(message) + text_field = v.TextField( + v_model=reactive_value.value, + on_v_model=on_v_model, + label=label, + disabled=disabled, + type="password" if password else None, + error=bool(error), + messages=messages, + class_=class_name, + style_=style, + ) + use_change(text_field, set_value_cast, enabled=not continuous_update, update_events=update_events) + return text_field + + +def use_change(el: reacton.core.Element, on_value: Callable[[Any], Any], enabled=True, update_events=["blur", "keyup.enter"]): + """Trigger a callback when a blur events occurs or the enter key is pressed.""" + on_value_ref = widget.use_ref(on_value) + on_value_ref.current = on_value + + def add_events(): + def on_change(widget, event, data): + if enabled: + on_value_ref.current(widget.v_model) + + import nextpy.interfaces.jupyter as widget + widget = cast(ipyvue.VueWidget, widget.get_widget(el)) + if enabled: + for event in update_events: + widget.on_event(event, on_change) + + def cleanup(): + if enabled: + for event in update_events: + widget.on_event(event, on_change, remove=True) + + return cleanup + + widget.use_effect(add_events, [enabled]) + + +def _use_input_type( + input_value: Union[None, T, widget.Reactive[Optional[T]], widget.Reactive[T]], + parse: Callable[[Optional[str]], T], + stringify: Callable[[Optional[T]], str], + on_value: Union[None, Callable[[Optional[T]], None], Callable[[T], None]] = None, +): + reactive_value = widget.use_reactive(input_value, on_value) # type: ignore + del input_value, on_value + string_value, set_string_value = widget.use_state(stringify(reactive_value.value) if reactive_value.value is not None else None) + # Use a ref to make sure sync_back_input_value() does not get a stale string_value + string_value_ref = widget.use_ref(string_value) + string_value_ref.current = string_value + + error_message = cast(Union[str, None], None) + + try: + reactive_value.set(parse(string_value)) + except ValueError as e: + error_message = str(e.args[0]) + + def sync_back_input_value(): + def on_external_value_change(new_value: Optional[T]): + new_string_value = stringify(new_value) + try: + parse(string_value_ref.current) + except ValueError: + # String value could be invalid when external value is changed by a different component + set_string_value(new_string_value) + else: + if new_value != parse(string_value_ref.current): + set_string_value(new_string_value) + + return reactive_value.subscribe(on_external_value_change) + + widget.use_effect(sync_back_input_value, [reactive_value]) + + return string_value, error_message, set_string_value diff --git a/nextpy/interfaces/jupyter/components/input_widgets/link_button.py b/nextpy/interfaces/jupyter/components/input_widgets/link_button.py new file mode 100644 index 00000000..ec7364d2 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/link_button.py @@ -0,0 +1,54 @@ +from typing import Dict, List, Union + +import ipyvue as vue +import reacton.ipyvue as vuer + +import nextpy.interfaces.jupyter as widget + + +@widget.component +def link_button( + path_or_route: Union[str, widget.Route], + children=[], + nofollow=False, + style: str = '', + class_name: str = '', +): + """Makes clicking on child elements navigate to a route. + + See also: + + * [Multipage](/docs/howto/multipage). + * [Understanding Routing](/docs/understanding/routing). + + Most common usage is in combination with a button, e.g.: + + ```python + with widget.Link("/fruit/banana"): + widget.button("Go to banana") + ``` + + + ## Arguments + + * path_or_route: the path or route to navigate to. Paths should be absolute, e.g. '/fruit/banana'. + If a route is given, [`resolve_path`](/api/resolve_path)] will be used to resolve to the absolute path. + * children: the children of the link. If a child is clicked, the link will be followed. + * nofollow: If True, the link will not be followed by web crawlers (such as google). + * style: CSS styles to apply to the html link element. Either a string or a dictionary. + * classes: A list of CSS classes to apply to the link. + + """ + path = widget.resolve_path(path_or_route, level=0) + attributes = {"href": path} + if nofollow: + attributes["rel"] = "nofollow" + + link = vue.Html.element(tag="a", children=children, attributes=attributes, style_=style, class_=class_name) + location = widget.use_context(widget.routing._location_context) + + def go(*ignore): + location.pathname = path + + vuer.use_event(link, "click.prevent.stop", go) + return link diff --git a/nextpy/interfaces/jupyter/components/input_widgets/multiselect.py b/nextpy/interfaces/jupyter/components/input_widgets/multiselect.py new file mode 100644 index 00000000..edc06a6f --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/multiselect.py @@ -0,0 +1,67 @@ +from typing import Callable, Dict, List, Optional, TypeVar, Union, cast, overload + +import ipyvuetify as v +import reacton.core + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.alias import rv +from nextpy.interfaces.jupyter.util import _combine_classes + +T = TypeVar("T") + +@widget.value_component(None) +def multiselect( + label: str, + values: List[T], + all_values: List[T], + on_value: Callable[[List[T]], None] = None, + dense: bool = False, + disabled: bool = False, + class_name: str = '', + style: str = '', +) -> reacton.core.ValueElement[v.Select, List[T]]: + """Select multiple values from a list of values. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + + all_languages = "Python C++ Java JavaScript TypeScript BASIC".split() + languages = widget.reactive([all_languages[0]]) + + + @widget.component + def Page(): + widget.SelectMultiple("Languages", languages, all_languages) + widget.Markdown(f"**Selected**: {languages.value}") + ``` + + ## Arguments + + * `label`: Label to display next to the select. + * `values`: List of currently selected values. + * `all_values`: List of all values to select from. + * `on_value`: Callback to call when the value changes. + * `dense`: Whether to use a denser style. + * `disabled`: Whether the select widget allow user interaction + * `classes`: List of CSS classes to apply to the select. + * `style`: CSS style to apply to the select. + """ + reactive_values = widget.use_reactive(values, on_value) + del values, on_value + + return cast( + reacton.core.ValueElement[v.Select, List[T]], + rv.Select( + v_model=reactive_values.value, + on_v_model=reactive_values.set, + items=all_values, + label=label, + multiple=True, + dense=False, + disabled=disabled, + class_=class_name, + style_=style, + ), + ) diff --git a/nextpy/interfaces/jupyter/components/input_widgets/number_input.py b/nextpy/interfaces/jupyter/components/input_widgets/number_input.py new file mode 100644 index 00000000..0ade8034 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/number_input.py @@ -0,0 +1,271 @@ +from typing import Any, Callable, Optional, TypeVar, Union, cast, overload, List, Dict + +import ipyvue +import ipyvuetify as vw +import reacton +from typing_extensions import Literal + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.alias import rv as v + +T = TypeVar("T") + +@widget.component +def input_float( + label: str, + value: Union[None, float, widget.Reactive[float], widget.Reactive[Optional[float]]] = 0, + on_value: Union[None, Callable[[Optional[float]], None], Callable[[float], None]] = None, + disabled: bool = False, + optional: bool = False, + continuous_update: bool = False, + clearable: bool = False, + class_name : str = '', + style: str = '' +): + """Numeric input (floats). + + Basic example: + + ```widget + import nextpy.interfaces.jupyter + + float_value = widget.reactive(42.0) + continuous_update = widget.reactive(True) + + @widget.component + def Page(): + widget.Checkbox(label="Continuous update", value=continuous_update) + widget.InputFloat("Enter a float number", value=float_value, continuous_update=continuous_update.value) + with widget.row(): + widget.button("Clear", on_click=lambda: float_value.set(42.0)) + widget.Markdown(f"**You entered**: {float_value.value}") + ``` + + + ## Arguments + + * `label`: Label to display next to the slider. + * `value`: The currently entered value. + * `on_value`: Callback to call when the value changes. + * `disabled`: Whether the input is disabled. + * `optional`: Whether the value can be None. + * `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed. + * `clearable`: Whether the input can be cleared. + * `classes`: List of CSS classes to apply to the input. + * `style`: CSS style to apply to the input. + + """ + + def str_to_float(value: Optional[str]): + if value: + try: + value = value.replace(",", ".") + return float(value) + except ValueError: + raise ValueError("Value must be a number") + else: + if optional: + return None + else: + raise ValueError("Value cannot be empty") + + return _InputNumeric( + str_to_float, + label=label, + value=value, + on_value=on_value, + disabled=disabled, + continuous_update=continuous_update, + clearable=clearable, + classes=class_name, + style=style, + ) + + +@widget.component +def input_int( + label: str, + value: Union[None, int, widget.Reactive[int], widget.Reactive[Optional[int]]] = 0, + on_value: Union[None, Callable[[Optional[int]], None], Callable[[int], None]] = None, + disabled: bool = False, + optional: bool = False, + continuous_update: bool = False, + clearable: bool = False, + classes: List[str] = [], + style: Optional[Union[str, Dict[str, str]]] = None, +): + """Numeric input (integers). + + Basic example: + + ```widget + import nextpy.interfaces.jupyter + + int_value = widget.reactive(42) + continuous_update = widget.reactive(True) + + @widget.component + def Page(): + widget.Checkbox(label="Continuous update", value=continuous_update) + widget.InputInt("Enter an integer number", value=int_value, continuous_update=continuous_update.value) + with widget.row(): + widget.button("Clear", on_click=lambda: int_value.set(42)) + widget.Markdown(f"**You entered**: {int_value.value}") + ``` + + ## Arguments + + * `label`: Label to display next to the slider. + * `value`: The currently entered value. + * `on_value`: Callback to call when the value changes. + * `disabled`: Whether the input is disabled. + * `optional`: Whether the value can be None. + * `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed. + * `clearable`: Whether the input can be cleared. + * `classes`: List of CSS classes to apply to the input. + * `style`: CSS style to apply to the input. + """ + + def str_to_int(value: Optional[str]): + if value: + try: + return int(value) + except ValueError: + raise ValueError("Value must be an integer") + else: + if optional: + return None + else: + raise ValueError("Value cannot be empty") + + return _InputNumeric( + str_to_int, + label=label, + value=value, + on_value=on_value, + disabled=disabled, + continuous_update=continuous_update, + clearable=clearable, + classes=classes, + style=style, + ) + + +def _use_input_type( + input_value: Union[None, T, widget.Reactive[Optional[T]], widget.Reactive[T]], + parse: Callable[[Optional[str]], T], + stringify: Callable[[Optional[T]], str], + on_value: Union[None, Callable[[Optional[T]], None], Callable[[T], None]] = None, +): + reactive_value = widget.use_reactive(input_value, on_value) # type: ignore + del input_value, on_value + string_value, set_string_value = widget.use_state(stringify(reactive_value.value) if reactive_value.value is not None else None) + # Use a ref to make sure sync_back_input_value() does not get a stale string_value + string_value_ref = widget.use_ref(string_value) + string_value_ref.current = string_value + + error_message = cast(Union[str, None], None) + + try: + reactive_value.set(parse(string_value)) + except ValueError as e: + error_message = str(e.args[0]) + + def sync_back_input_value(): + def on_external_value_change(new_value: Optional[T]): + new_string_value = stringify(new_value) + try: + parse(string_value_ref.current) + except ValueError: + # String value could be invalid when external value is changed by a different component + set_string_value(new_string_value) + else: + if new_value != parse(string_value_ref.current): + set_string_value(new_string_value) + + return reactive_value.subscribe(on_external_value_change) + + widget.use_effect(sync_back_input_value, [reactive_value]) + + return string_value, error_message, set_string_value + + +@widget.component +def _InputNumeric( + str_to_numeric: Callable[[Optional[str]], T], + label: str, + value: Union[None, T, widget.Reactive[Optional[T]], widget.Reactive[T]], + on_value: Union[None, Callable[[Optional[T]], None], Callable[[T], None]] = None, + disabled: bool = False, + continuous_update: bool = False, + clearable: bool = False, + class_name: List[str] = [], + style: Optional[Union[str, Dict[str, str]]] = None, +): + """Numeric input. + + ## Arguments + + * `label`: Label to display next to the slider. + * `value`: The currently entered value. + * `on_value`: Callback to call when the value changes. + * `disabled`: Whether the input is disabled. + * `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed. + * `classes`: List of CSS classes to apply to the input. + * `style`: CSS style to apply to the input. + """ + + internal_value, error, set_value_cast = _use_input_type( + value, + str_to_numeric, + str, + on_value, + ) + + def on_v_model(value): + if continuous_update: + set_value_cast(value) + + if error: + label += f" ({error})" + text_field = v.TextField( + v_model=internal_value, + on_v_model=on_v_model, + label=label, + disabled=disabled, + # we are not using the number type, since we cannot validate invalid input + # see https://stackoverflow.blog/2022/12/26/why-the-number-input-is-the-worst-input/ + # type="number", + hide_details=True, + clearable=clearable, + error=bool(error), + class_=class_name, + style_=style, + ) + use_change(text_field, set_value_cast, enabled=not continuous_update) + return text_field + + +def use_change(el: reacton.core.Element, on_value: Callable[[Any], Any], enabled=True, update_events=["blur", "keyup.enter"]): + """Trigger a callback when a blur events occurs or the enter key is pressed.""" + on_value_ref = widget.use_ref(on_value) + on_value_ref.current = on_value + + def add_events(): + def on_change(widget, event, data): + if enabled: + on_value_ref.current(widget.v_model) + + widget = cast(ipyvue.VueWidget, widget.get_widget(el)) + if enabled: + for event in update_events: + widget.on_event(event, on_change) + + def cleanup(): + if enabled: + for event in update_events: + widget.on_event(event, on_change, remove=True) + + return cleanup + + widget.use_effect(add_events, [enabled]) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/input_widgets/radio.py b/nextpy/interfaces/jupyter/components/input_widgets/radio.py new file mode 100644 index 00000000..cd7264be --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/radio.py @@ -0,0 +1,47 @@ +import reacton.ipyvuetify as v +from typing import Optional, Union, Callable, List, Dict +import nextpy.interfaces.jupyter as widget + +@widget.component +def radio( + label: Optional[str] = None, + options: List[str] = [], + value: Optional[str] = None, + key: Optional[Union[str, int]] = None, + help: Optional[str] = None, + on_change: Optional[Callable] = None, + args: Optional[Dict] = None, + kwargs: Optional[Dict] = None, + disabled: bool = False, + horizontal: bool = False, + class_name: str = '', + style: str = '', + icons: Optional[List[str]] = None, + label_visibility: str = "visible", +): + if kwargs is None: + kwargs = {} + if label_visibility == "hidden": + label = "" + elif label_visibility == "collapsed": + class_name += " no-label" + + radio_buttons = [ + v.Radio(label=f"{option} {icons[idx] if icons else ''}", value=option, disabled=disabled) + for idx, option in enumerate(options) + ] + + radio_group = v.RadioGroup( + v_model=value, + children=radio_buttons, + row=horizontal, + class_=class_name, + style_=style, + **kwargs + ) + if on_change: + radio_group.on_event('change', lambda *args, **kwargs: on_change(*args, **kwargs)) + + container = v.Container(children=[v.Html(tag='h3', children=[label]), radio_group]) + + return container diff --git a/nextpy/interfaces/jupyter/components/input_widgets/select_box.py b/nextpy/interfaces/jupyter/components/input_widgets/select_box.py new file mode 100644 index 00000000..47268eb5 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/select_box.py @@ -0,0 +1,69 @@ +from typing import Callable, Dict, List, Optional, TypeVar, Union, cast, overload + +import ipyvuetify as v +import reacton.core + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.alias import rv +from nextpy.interfaces.jupyter.util import _combine_classes + +T = TypeVar("T") + + +@widget.value_component(None) +def select_box( + label: str, + values: List[T], + value: Union[None, T, widget.Reactive[T], widget.Reactive[Optional[T]]] = None, + on_value: Union[None, Callable[[T], None], Callable[[Optional[T]], None]] = None, + dense: bool = False, + disabled: bool = False, + class_name : str = '', + style: str = '' +) -> reacton.core.ValueElement[v.Select, T]: + """Select a single value from a list of values. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + + foods = ["Kiwi", "Banana", "Apple"] + food = widget.reactive("Banana") + + + @widget.component + def Page(): + widget.Select(label="Food", value=food, values=foods) + widget.Markdown(f"**Selected**: {food.value}") + ``` + + ## Arguments + + * `label`: Label to display next to the select. + * `value`: The currently selected value. + * `values`: List of values to select from. + * `on_value`: Callback to call when the value changes. + * `dense`: Whether to use a denser style. + * `disabled`: Whether the select widget allow user interaction + * `classes`: List of CSS classes to apply to the select. + * `style`: CSS style to apply to the select. + + """ + # next line is very hard to get right with typing + # might need an overload on use_reactive, when value is None + reactive_value = widget.use_reactive(value, on_value) # type: ignore + del value, on_value + return cast( + reacton.core.ValueElement[v.Select, T], + rv.Select( + v_model=reactive_value.value, + on_v_model=reactive_value.set, + items=values, + label=label, + dense=dense, + disabled=disabled, + class_=class_name, + style_=style, + ), + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/input_widgets/select_slider.py b/nextpy/interfaces/jupyter/components/input_widgets/select_slider.py new file mode 100644 index 00000000..818f3066 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/select_slider.py @@ -0,0 +1,181 @@ +import math +import os +from datetime import date, datetime, timedelta +from typing import Callable, List, Optional, Tuple, TypeVar, Union, cast + +import ipyvue +import ipyvuetify +import reacton.core +import traitlets +from typing_extensions import Literal + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.alias import rv + +T = TypeVar("T") + + +@widget.value_component(None) +def select_slider( + label: str, + value: Union[Tuple[int, int], widget.Reactive[Tuple[int, int]]] = (1, 3), + min: int = 0, + max: int = 10, + step: int = 1, + on_value: Callable[[Tuple[int, int]], None] = None, + thumb_label: Union[bool, Literal["always"]] = True, + tick_labels: Union[List[str], Literal["end_points"], bool] = False, + disabled: bool = False, + class_name : str = '', + style: str = '' +) -> reacton.core.ValueElement[ipyvuetify.RangeSlider, Tuple[int, int]]: + """Slider for controlling a range of integer values. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + + int_range = widget.reactive((0, 42)) + + + @widget.component + def Page(): + widget.SliderRangeInt("Some integer range", value=int_range, min=-10, max=120) + widget.Markdown(f"**Int range value**: {int_range.value}") + with widget.row(): + widget.button("Reset", on_click=lambda: int_range.set((0, 42))) + ``` + + ## Arguments + * `label`: Label to display next to the slider. + * `value`: The currently selected value. + * `min`: Minimum value. + * `max`: Maximum value. + * `step`: Step size. + * `on_value`: Callback to call when the value changes. + * `thumb_label`: Show a thumb label when sliding (True), always ("always"), or never (False). + * `tick_labels`: Show tick labels corresponding to the values (True), + custom tick labels by passing a list of strings, only end_points ("end_points"), + or no labels at all (False, the default). + * `disabled`: Whether the slider is disabled. + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + + def set_value_cast(value): + v1, v2 = value + reactive_value.set((int(v1), int(v2))) + + updated_tick_labels = _produce_tick_labels(tick_labels, min, max, step) + + return cast( + reacton.core.ValueElement[ipyvuetify.RangeSlider, Tuple[int, int]], + rv.RangeSlider( + v_model=reactive_value.value, + on_v_model=set_value_cast, + label=label, + min=min, + max=max, + step=step, + thumb_label=thumb_label, + tick_labels=updated_tick_labels, + dense=False, + hide_details=True, + disabled=disabled, + class_=class_name, + style_=style + ), + ) + +@widget.value_component(None) +def select_slider_float( + label: str, + value: Union[Tuple[float, float], widget.Reactive[Tuple[float, float]]] = (1.0, 3.0), + min: float = 0.0, + max: float = 10.0, + step: float = 0.1, + on_value: Callable[[Tuple[float, float]], None] = None, + thumb_label: Union[bool, Literal["always"]] = True, + tick_labels: Union[List[str], Literal["end_points"], bool] = False, + disabled: bool = False, + class_name : str = '', + style: str = '' +) -> reacton.core.ValueElement[ipyvuetify.RangeSlider, Tuple[float, float]]: + """Slider for controlling a range of float values. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + + float_range = widget.reactive((0.1, 42.4)) + + + @widget.component + def Page(): + widget.SliderRangeFloat("Some float range", value=float_range, min=-10, max=120) + widget.Markdown(f"**Float range value**: {float_range.value}") + with widget.row(): + widget.button("Reset", on_click=lambda: float_range.set((0.1, 42.4))) + ``` + + ## Arguments + * `label`: Label to display next to the slider. + * `value`: The current value. + * `min`: The minimum value. + * `max`: The maximum value. + * `step`: The step size. + * `on_value`: Callback to call when the value changes. + * `thumb_label`: Show a thumb label when sliding (True), always ("always"), or never (False). + * `tick_labels`: Show tick labels corresponding to the values (True), + custom tick labels by passing a list of strings, only end_points ("end_points"), + or no labels at all (False, the default). + * `disabled`: Whether the slider is disabled. + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + + def set_value_cast(value): + v1, v2 = value + reactive_value.set((float(v1), float(v2))) + + updated_tick_labels = _produce_tick_labels(tick_labels, min, max, step) + + return cast( + reacton.core.ValueElement[ipyvuetify.RangeSlider, Tuple[float, float]], + rv.RangeSlider( + v_model=reactive_value.value, + on_v_model=set_value_cast, + label=label, + min=min, + max=max, + step=step, + thumb_label=thumb_label, + tick_labels=updated_tick_labels, + dense=False, + hide_details=True, + disabled=disabled, + class_=class_name, + style_=style + ), + ) + + +def _produce_tick_labels(tick_labels: Union[List[str], Literal["end_points"], bool], min: float, max: float, step: float) -> Optional[List[str]]: + if tick_labels == "end_points": + num_repeats = int(math.ceil((max - min) / step)) - 1 + _tick_labels = [str(min), *([""] * num_repeats), str(max)] + elif tick_labels is False: + _tick_labels = None + elif tick_labels is True: + _tick_labels, start = [], min + + while start < max: + _tick_labels.append(str(start)) + start += step + _tick_labels.append(str(max)) + else: + _tick_labels = tick_labels + + return _tick_labels \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/input_widgets/slider.py b/nextpy/interfaces/jupyter/components/input_widgets/slider.py new file mode 100644 index 00000000..b5b02ed5 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/slider.py @@ -0,0 +1,317 @@ +import math +import os +from datetime import date, datetime, timedelta +from typing import Callable, List, Optional, Tuple, TypeVar, Union, cast + +import ipyvue +import ipyvuetify +import reacton.core +import traitlets +from typing_extensions import Literal + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.alias import rv + +T = TypeVar("T") + + +@widget.value_component(int) +def slider( + label: str, + value: Union[int, widget.Reactive[int]] = 0, + min: int = 0, + max: int = 10, + step: int = 1, + on_value: Optional[Callable[[int], None]] = None, + thumb_label: Union[bool, Literal["always"]] = True, + tick_labels: Union[List[str], Literal["end_points"], bool] = False, + disabled: bool = False, + class_name : str = '', + style: str = '' +): + """Slider for controlling an integer value. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + + int_value = widget.reactive(42) + + + @widget.component + def Page(): + widget.SliderInt("Some integer", value=int_value, min=-10, max=120) + widget.Markdown(f"**Int value**: {int_value.value}") + with widget.row(): + widget.button("Reset", on_click=lambda: int_value.set(42)) + ``` + + + ## Arguments + + * `label`: Label to display next to the slider. + * `value`: The currently selected value. + * `min`: Minimum value. + * `max`: Maximum value. + * `step`: Step size. + * `on_value`: Callback to call when the value changes. + * `thumb_label`: Show a thumb label when sliding (True), always ("always"), or never (False). + * `tick_labels`: Show tick labels corresponding to the values (True), + custom tick labels by passing a list of strings, only end_points ("end_points"), + or no labels at all (False, the default). + * `disabled`: Whether the slider is disabled. + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + + def set_value_cast(value): + reactive_value.value = int(value) + + updated_tick_labels = _produce_tick_labels(tick_labels, min, max, step) + + return rv.Slider( + v_model=reactive_value.value, + on_v_model=set_value_cast, + label=label, + min=min, + max=max, + step=step, + thumb_label=thumb_label, + tick_labels=updated_tick_labels, + dense=False, + hide_details=True, + disabled=disabled, + class_=class_name, + style_=style + ) + + +@widget.value_component(float) +def slider_float( + label: str, + value: Union[float, widget.Reactive[float]] = 0, + min: float = 0, + max: float = 10.0, + step: float = 0.1, + on_value: Callable[[float], None] = None, + thumb_label: Union[bool, Literal["always"]] = True, + tick_labels: Union[List[str], Literal["end_points"], bool] = False, + disabled: bool = False, + class_name : str = '', + style: str = '' +): + """Slider for controlling a float value. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + + float_value = widget.reactive(42.4) + + + @widget.component + def Page(): + widget.SliderFloat("Some integer", value=float_value, min=-10, max=120) + widget.Markdown(f"**Float value**: {float_value.value}") + with widget.row(): + widget.button("Reset", on_click=lambda: float_value.set(42.5)) + ``` + + ## Arguments + * `label`: Label to display next to the slider. + * `value`: The current value. + * `min`: The minimum value. + * `max`: The maximum value. + * `step`: The step size. + * `on_value`: Callback to call when the value changes. + * `thumb_label`: Show a thumb label when sliding (True), always ("always"), or never (False). + * `tick_labels`: Show tick labels corresponding to the values (True), + custom tick labels by passing a list of strings, only end_points ("end_points"), + or no labels at all (False, the default). + * `disabled`: Whether the slider is disabled. + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + + def set_value_cast(value): + reactive_value.set(float(value)) + + updated_tick_labels = _produce_tick_labels(tick_labels, min, max, step) + + return rv.Slider( + v_model=reactive_value.value, + on_v_model=set_value_cast, + label=label, + min=min, + max=max, + step=step, + thumb_label=thumb_label, + tick_labels=updated_tick_labels, + dense=False, + hide_details=True, + disabled=disabled, + class_=class_name, + style_=style + ) + + +@widget.value_component(None) +def slider_value( + label: str, + value: Union[T, widget.Reactive[T]], + values: List[T], + on_value: Callable[[T], None] = None, + disabled: bool = False, + class_name : str = '', + style: str = '' +) -> reacton.core.ValueElement[ipyvuetify.Slider, T]: + """Slider for selecting a value from a list of values. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + + foods = ["Kiwi", "Banana", "Apple"] + food = widget.reactive("Banana") + + + @widget.component + def Page(): + widget.SliderValue(label="Food", value=food, values=foods) + widget.Markdown(f"**Selected**: {food.value}") + ``` + + ## Arguments + * `label`: Label to display next to the slider. + * `value`: The currently selected value. + * `values`: List of values to select from. + * `on_value`: Callback to call when the value changes. + * `disabled`: Whether the slider is disabled. + + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + index, set_index = widget.use_state(values.index(reactive_value.value), key="index") + + def on_index(index): + set_index(index) + value = values[index] + reactive_value.set(value) + + return cast( + reacton.core.ValueElement[ipyvuetify.Slider, T], + rv.Slider( + v_model=index, + on_v_model=on_index, + ticks=True, + tick_labels=values, + label=label, + min=0, + max=len(values) - 1, + dense=False, + hide_details=True, + disabled=disabled, + class_=class_name, + style_=style + ), + ) + + +class DateSliderWidget(ipyvue.VueTemplate): + template_file = os.path.realpath(os.path.join(os.path.dirname(__file__), "slider_date.vue")) + + min = traitlets.CFloat(0).tag(sync=True) + days = traitlets.CFloat(0).tag(sync=True) + value = traitlets.Any(0).tag(sync=True) + + label = traitlets.Unicode("").tag(sync=True) + disabled = traitlets.Bool(False).tag(sync=True) + + +@widget.value_component(date) +def slider_date( + label: str, + value: Union[date, widget.Reactive[date]] = date(2010, 7, 28), + min: date = date(1981, 1, 1), + max: date = date(2050, 12, 30), + on_value: Callable[[date], None] = None, + disabled: bool = False, + class_name : str = '', + style: str = '' +): + """Slider for controlling a date value. + + ### Basic example: + + ```widget + import nextpy.interfaces.jupyter + import datetime + + date_value = widget.reactive(datetime.date(2010, 7, 28)) + + + @widget.component + def Page(): + widget.SliderDate("Some date", value=date_value) + widget.Markdown(f"**Date value**: {date_value.value.strftime('%Y-%b-%d')}") + with widget.row(): + widget.button("Reset", on_click=lambda: date_value.set(datetime.date(2010, 7, 28))) + ``` + + ## Arguments + * `label`: Label to display next to the slider. + * `value`: The current value. + * `min`: The minimum value. + * `max`: The maximum value. + * `on_value`: Callback to call when the value changes. + * `disabled`: Whether the slider is disabled. + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + + def format(d: date): + return float(datetime(d.year, d.month, d.day).timestamp()) + + dt_min = format(min) + delta: timedelta = max - min + days = delta.days + + delta_value: timedelta = reactive_value.value - min + days_value = delta_value.days + if days_value < 0: + days_value = 0 + + def set_value_cast(value): + date = min + timedelta(days=value) + reactive_value.set(date) + + return DateSliderWidget.element(label=label, min=dt_min, days=days, on_value=set_value_cast, value=days_value, + disabled=disabled, class_=class_name, style_=style) + + +def _produce_tick_labels(tick_labels: Union[List[str], Literal["end_points"], bool], min: float, max: float, step: float) -> Optional[List[str]]: + if tick_labels == "end_points": + num_repeats = int(math.ceil((max - min) / step)) - 1 + _tick_labels = [str(min), *([""] * num_repeats), str(max)] + elif tick_labels is False: + _tick_labels = None + elif tick_labels is True: + _tick_labels, start = [], min + + while start < max: + _tick_labels.append(str(start)) + start += step + _tick_labels.append(str(max)) + else: + _tick_labels = tick_labels + + return _tick_labels + + +# FloatSlider = SliderFloat +# IntSlider = SliderInt +# ValueSlider = SliderValue +# DateSlider = SliderDate diff --git a/nextpy/interfaces/jupyter/components/input_widgets/slider_date.vue b/nextpy/interfaces/jupyter/components/input_widgets/slider_date.vue new file mode 100644 index 00000000..a96ea378 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/slider_date.vue @@ -0,0 +1,56 @@ + + + diff --git a/nextpy/interfaces/jupyter/components/input_widgets/switch.py b/nextpy/interfaces/jupyter/components/input_widgets/switch.py new file mode 100644 index 00000000..5586459a --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/switch.py @@ -0,0 +1,68 @@ +from typing import Callable, Dict, List, Optional, Union + +import reacton.ipyvuetify as v + +import nextpy.interfaces.jupyter as widget + + +@widget.value_component(bool) +def switch( + *, + label: str = None, + value: Union[bool, widget.Reactive[bool]] = True, + on_value: Callable[[bool], None] = None, + disabled: bool = False, + children: list = [], + classes: List[str] = [], + style: Optional[Union[str, Dict[str, str]]] = None, +): + """A switch component provides users the ability to choose between two distinct values. But aesthetically different from a checkbox. + + Basic examples + + ```widget + import nextpy.interfaces.jupyter + + show_message = widget.reactive(True) + disable = widget.reactive(False) + + + @widget.component + def Page(): + with widget.column(): + with widget.row(): + widget.switch(label="Hide Message", value=show_message, disabled=disable.value) + widget.switch(label="Disable Message switch", value=disable) + + if show_message.value: + widget.Markdown("## Use switch to show/hide message") + + ``` + + + ## Arguments + + * `label`: The label to display next to the switch. + * `value`: The current value of the switch (True or False). + * `on_value`: A callback that is called when the switch is toggled. + * `disabled`: If True, the switch is disabled and cannot be used. + * `children`: A list of child elements to display on the switch. + * `classes`: Additional CSS classes to apply. + * `style`: CSS style to apply. + + """ + reactive_value = widget.use_reactive(value, on_value) + del value, on_value + + if label: + children = [label] + children + + return v.Switch( + label=label, + v_model=reactive_value.value, + on_v_model=reactive_value.set, + disabled=disabled, + class_=widget.util._combine_classes(classes), + style_=widget.util._flatten_style(style), + children=children, + ) diff --git a/nextpy/interfaces/jupyter/components/input_widgets/textarea.py b/nextpy/interfaces/jupyter/components/input_widgets/textarea.py new file mode 100644 index 00000000..cf3c8608 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/textarea.py @@ -0,0 +1,47 @@ +import reacton.ipyvuetify as v +from typing import Optional, Union, Callable, Tuple, Dict +import nextpy.interfaces.jupyter as widget + +@widget.component +def textarea( + label: Optional[str] = None, + value: str = "", + max_chars: Optional[int] = None, + key: Optional[Union[str, int]] = None, + type: str = "default", + help: Optional[str] = None, + autocomplete: Optional[str] = None, + on_change: Optional[Callable] = None, + args: Optional[Tuple] = None, + kwargs: Optional[Dict] = None, + placeholder: Optional[str] = None, + disabled: bool = False, + label_visibility: str = "visible", + class_name: str = '', + style: str = '', +): + if kwargs is None: + kwargs = {} + if label_visibility == "hidden": + label = "" + elif label_visibility == "collapsed": + class_name += " no-label" + + component_kwargs = { + "label": label, + "v_model": value, + "maxlength": max_chars, + "type": type, + "hint": help, + "persistent_hint": True, + "placeholder": placeholder, + "disabled": disabled, + "class_": class_name, + "style_": style, + **kwargs + } + + if on_change: + component_kwargs["on_change"] = lambda *args, **kwargs: on_change(*args, **kwargs) + + return v.Textarea(**component_kwargs) diff --git a/nextpy/interfaces/jupyter/components/input_widgets/time_picker.py b/nextpy/interfaces/jupyter/components/input_widgets/time_picker.py new file mode 100644 index 00000000..25eda03a --- /dev/null +++ b/nextpy/interfaces/jupyter/components/input_widgets/time_picker.py @@ -0,0 +1,26 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget +import datetime + +@widget.component +def time_picker( + label: Optional[str] = None, + value: Optional[Union[datetime.time, str]] = "now", + class_name: str = '', + style: str = '', + **kwargs, +): + if isinstance(value, datetime.time): + value = value.strftime('%H:%M') + + if value == "now": + value = datetime.datetime.now().strftime('%H:%M') + + return v.TimePicker( + label=label, + v_model=value, + class_=class_name, + style_=style, + **kwargs, + ) diff --git a/nextpy/interfaces/jupyter/components/media/__init__.py b/nextpy/interfaces/jupyter/components/media/__init__.py new file mode 100644 index 00000000..0052f819 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/media/__init__.py @@ -0,0 +1,3 @@ +from .audio import audio +from .image import image +from .video import video \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/media/audio.py b/nextpy/interfaces/jupyter/components/media/audio.py new file mode 100644 index 00000000..255c3db7 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/media/audio.py @@ -0,0 +1,21 @@ +import reacton.ipyvuetify as v +import nextpy.interfaces.jupyter as widget + +@widget.component +def audio( + src: str = '', + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag='audio', + attributes={ + 'controls': True, + 'src': src, + **kwargs + }, + class_=class_name, + style_=style + ) + diff --git a/nextpy/interfaces/jupyter/components/media/image.py b/nextpy/interfaces/jupyter/components/media/image.py new file mode 100644 index 00000000..af7b3a0d --- /dev/null +++ b/nextpy/interfaces/jupyter/components/media/image.py @@ -0,0 +1,21 @@ +import reacton.ipyvuetify as v +import nextpy.interfaces.jupyter as widget + +@widget.component +def image( + src: str, + alt: str = '', + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag='img', + attributes={ + 'src': src, + 'alt': alt, + **kwargs + }, + class_=class_name, + style_=style + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/media/video.py b/nextpy/interfaces/jupyter/components/media/video.py new file mode 100644 index 00000000..3aac5e98 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/media/video.py @@ -0,0 +1,26 @@ +import reacton.ipyvuetify as v +import nextpy.interfaces.jupyter as widget + +@widget.component +def video( + src: str = '', + class_name: str = '', + style: str = '', + controls: bool = True, # Show video controls + autoplay: bool = True, # Autoplay video + loop: bool = False, # Loop video + **kwargs +): + return v.Html( + tag='video', + attributes={ + 'src': src, + 'controls': controls, + 'autoplay': autoplay, + 'loop': loop, + **kwargs + }, + children=[], + class_=class_name, + style_=style + ) diff --git a/nextpy/interfaces/jupyter/components/misc.py b/nextpy/interfaces/jupyter/components/misc.py new file mode 100644 index 00000000..a64b3869 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/misc.py @@ -0,0 +1,357 @@ +import warnings +from typing import Any, Callable, Dict, List, Union + +import reacton +import reacton.ipyvuetify as v + +import nextpy.interfaces.jupyter as widget +import nextpy.interfaces.jupyter.widgets +from nextpy.interfaces.jupyter.util import _combine_classes + +Navigator = reacton.core.ComponentWidget(widget.widgets.Navigator) +GridDraggable = reacton.core.ComponentWidget(widget.widgets.GridLayout) +# keep the old name for a while +GridLayout = GridDraggable + + +@widget.component +def ListItem(title, icon_name: str = None, children=[], value=None): + if value is None: + value = title + if children: + with v.ListItemContent() as main: + v.ListItemTitle(children=[title]) + if icon_name is not None: + with v.ListItemIcon(): + v.Icon(children=[icon_name or ""]) + return v.ListGroup(children=children, v_slots=[{"name": "activator", "children": main}], no_action=True, value=True, append_icon=icon_name) + else: + with v.ListItem(value=value) as main: + if icon_name is not None: + with v.ListItemIcon(): + v.Icon(children=[icon_name or ""]) + with v.ListItemContent(): + v.ListItemTitle(children=[title]) + return main + + +def ui_dropdown(label, value=None, options=["foo", "bar"], key=None, disabled=False, **kwargs): + key = key or str(value) + str(label) + str(options) + value, set_value = widget.use_state(value, key) + + def set_index(index): + set_value(options[index]) + + v.Select(v_model=value, label=label, items=options, on_v_model=set_value, clearable=True, disabled=disabled, **kwargs) + return value + + +def ui_text(label, value="", key=None, clearable=False, hint="", disabled=False, **kwargs): + key = key or str(value) + str(label) + str(hint) + value, set_value = widget.use_state(value, key) + v.TextField(v_model=value, label=label, on_v_model=set_value, clearable=clearable, hint=hint, disabled=disabled, **kwargs) + return value + + +def ui_checkbox(label, value=True, key=None, disabled=False, **kwargs): + key = key or str(value) + str(label) + value, set_value = widget.use_state(value, key) + v.Checkbox(v_model=value, label=label, on_v_model=set_value, **kwargs) + return value + + +def ui_slider(value=1, label="", min=0, max=100, key=None, tick_labels=None, thumb_label=None, disabled=False, **kwargs): + key = key or str(value) + str(label) + value, set_value = widget.use_state(value, key) + v.Slider( + v_model=value, + label=label, + min=min, + max=max, + on_v_model=set_value, + ticks=tick_labels is not None, + tick_labels=tick_labels, + thumb_label=thumb_label, + disabled=disabled, + **kwargs, + ) + return value + + +@widget.component +def text(text, style: Union[str, Dict[str, str], None] = None, classes: List[str] = []): + style_flat = widget.util._flatten_style(style) + return v.Html(tag="span", class_=_combine_classes(classes), style_=style_flat, children=[text]) + + +@widget.component +def div(children=[], classes: List[str] = [], style: Union[str, Dict[str, str], None] = None, **kwargs): + style_flat = widget.util._flatten_style(style) + classes = classes.copy() + kwargs = kwargs.copy() + if "class_" in kwargs: + classes.append(kwargs.pop("class_")) + if "style_" in kwargs: + style_flat += kwargs.pop("style_") + class_ = _combine_classes(classes) + + return v.Html(tag="div", children=children, class_=class_, style_=style_flat, **kwargs) + + +@widget.component +def pre(text, **kwargs): + return v.Html(tag="pre", children=[text], **kwargs) + + +@widget.component +def IconButton(icon_name: str = None, on_click=Callable[[], None], children: list = [], click_event="click", **kwargs): + return widget.button(icon_name=icon_name, on_click=on_click, children=children, icon=True, click_event=click_event, **kwargs) + + +@widget.component +def html(tag="div", unsafe_innerHTML=None, style: str = None, classes: List[str] = [], attributes=None, class_: str = None): + """Render an html tag with optional raw html text inside. + + # Arguments + + * `tag`: html tag name for the top level element (default: `div`) + * `unsafe_innerHTML`: html string to be rendered inside the tag. + Note that this is not sanitized, so be careful this cannot include JavaScript from user input! + * `style`: CSS style string to be applied to the top level element. + * `classes`: List of CSS classes to be applied to the top level element. + * `attributes`: Dictionary of attributes to be applied to the top level element. + * `class_`: (deprecated) CSS class to be applied to the top level element. + + """ + if attributes is None: + attributes = {} + else: + attributes = attributes.copy() + if style: + attributes["style"] = style + if class_ or classes: + class_ = _combine_classes([*classes, *([] if class_ is None else [class_])]) + attributes["class"] = class_ + return widget.widgets.html.element(tag=tag, unsafe_innerHTML=unsafe_innerHTML, attributes=attributes) + +@reacton.component +def head(children: List[reacton.core.Element] = []): + """A component that manager the "head" tag of the page to avoid duplicate tags, such as titles. + + Currently only supports the [title](/api/title) tag as child, e.g.: + + ```python + import nextpy.interfaces.jupyter + + @widget.component + def Page(): + with widget.vstack() as main: + MyAwesomeComponent() + with widget.head(): + widget.Title("My page title") + return main + + ``` + """ + return widget.div(children=children, style="display; none") + +@widget.component +def vstack(children=[], grow=True, align_items="stretch", classes: List[str] = []): + """Deprecated. Use `row` instead.""" + style = f"flex-direction: column; align-items: {align_items};" + if grow: + style += "flex-grow: 1;" + class_ = _combine_classes(["d-flex", *classes]) + return v.Sheet(class_=class_, style_=style, elevation=0, children=children) + + +@widget.component +def hstack(children=[], grow=True, align_items="stretch", classes: List[str] = []): + """Deprecated. Use `column` instead.""" + style = f"flex-direction: row; align-items: {align_items}; " + if grow: + style += "flex-grow: 1;" + class_ = _combine_classes(["d-flex", *classes]) + return v.Sheet(class_=class_, style_=style, elevation=0, children=children) + + +@widget.component +def row(children=[], gap="12px", justify="start", margin: int = 0, classes: List[str] = [], style: Union[str, Dict[str, str], None] = None): + """Lays out children in a row, side by side, with the given gap between them. + + See also [column](/api/column). + + Example with three children side by side: + + ```widget + import nextpy.interfaces.jupyter + + @widget.component + def Page(): + with widget.row(gap="10px", justify="space-around"): + widget.text("On the left") + widget.text("In the middle") + widget.text("On the right") + ``` + ## Arguments + + * `children`: List of children to render in the column. + * `gap`: The gap between each child, as a CSS string. + * `justify`: How children are distributed along the x/horizontal-axis, can be "start" (default), "center", "end", "space-around", + "space-between" or "space-evenly". + (*Note: this translates to justify-content in CSS*). + * `margin`: The margin around the column, translate to 4*margin pixels. + * `classes`: List of CSS classes to apply to the column. + * `style`: CSS style to apply to the column. + + """ + align_items = "stretch" + style_flat = widget.util._flatten_style(style) + style_flat = f"flex-direction: row; align-items: {align_items}; justify-content: {justify}; column-gap: {gap};" + style_flat + ";" + # valid css values, but we don't list them as options to avoid confusion + extra_justify_options = ["left", "right", "flex-start", "flex-end"] + if justify not in (["start", "center", "end", "space-around", "space-between", "space-evenly"] + extra_justify_options): + warnings.warn(f"Invalid value for justify: {justify}, possible values are: start, center, end, space-around, space-between, space-evenly") + class_ = _combine_classes(["d-flex", f"ma-{margin}", *classes]) + return v.Sheet(class_=class_, style_=style_flat, elevation=0, children=children) + + +@widget.component +def column(children=[], gap="12px", align="stretch", margin: int = 0, classes: List[str] = [], style: Union[str, Dict[str, str], None] = None): + """Lays out children in a column on top of each other, with the given gap between them. + + See also [row](/api/row). + + Example with three children on top of each other: + + ```widget + import nextpy.interfaces.jupyter + + @widget.component + def Page(): + with widget.column(gap="10px"): + widget.text("On top") + widget.text("In the middle") + widget.text("On bottom") + ``` + + ## Arguments + + * `children`: List of children to render in the column. + * `gap`: The gap between each child, as a CSS string. + * `align`: The alignment of the children, can be "start", "center", "end", "stretch" (default). + (*Note: this translates to align-items in CSS*). + * `margin`: The margin around the column, translate to 4*margin pixels. + * `classes`: List of CSS classes to apply to the column. + * `style`: CSS style to apply to the column. + + """ + if align == "left": + warnings.warn("align='left' does not exists, you probably want align='start' instead") + align = "start" + if align == "right": + warnings.warn("align='right' does not exists, you probably want align='end' instead") + align = "end" + # valid css options, but we don't list them as options to avoid confusion + extra_align_options = ["flex-start", "flex-end", "self-start", "self-end", "baseline"] + if align not in (["start", "center", "end", "stretch"] + extra_align_options): + warnings.warn(f"Invalid value for align: {align}, possible values are 'start', 'center', 'end' or 'stretch'") + style_flat = widget.util._flatten_style(style) + style_flat = f"flex-direction: column; align-items: {align}; row-gap: {gap};" + style_flat + ";" + class_ = _combine_classes(["d-flex", f"ma-{margin}", *classes]) + return v.Sheet(class_=class_, style_=style_flat, elevation=0, children=children) + + +@widget.component +def GridFixed(columns=4, column_gap="10px", row_gap="10px", children=[], align_items="stretch", justify_items="stretch"): + """ + + See css grid spec: + https://css-tricks.com/snippets/css/complete-guide-grid/ + """ + style = ( + f"display: grid; grid-template-columns: repeat({columns}, minmax(0, 1fr)); " + + f"grid-column-gap: {column_gap}; grid-row-gap: {row_gap}; align-items: {align_items}; justify-items: {justify_items}" + ) + return div(style_=style, children=children) + + +@widget.component +def padding(size, children=[], grow=True): + style = "flex-direction: row;" + if grow: + style += "flex-grow: 1;" + return v.Sheet(class_=f"pa-{size}", style_=style, elevation=0, children=children) + + +@widget.component +def FigurePlotly( + fig, + on_selection: Callable[[Any], None] = None, + on_deselect: Callable[[Any], None] = None, + on_click: Callable[[Any], None] = None, + on_hover: Callable[[Any], None] = None, + on_unhover: Callable[[Any], None] = None, + on_relayout: Callable[[Any], None] = None, + dependencies=None, +): + from plotly.graph_objs._figurewidget import FigureWidget + + def on_points_callback(data): + if not data: + return + + event_type = data["event_type"] + event_mapping = { + "plotly_click": on_click, + "plotly_hover": on_hover, + "plotly_unhover": on_unhover, + "plotly_selected": on_selection, + "plotly_deselect": on_deselect + } + + callback = event_mapping.get(event_type) + if callback: + callback(data) + + fig_element = FigureWidget.element( + on__js2py_pointsCallback=on_points_callback, + on__js2py_relayout=on_relayout + ) + + def update_data(): + fig_widget: FigureWidget = widget.get_widget(fig_element) + fig_widget.layout = fig.layout + + length = len(fig_widget.data) + fig_widget.add_traces(fig.data) + data = list(fig_widget.data) + fig_widget.data = data[length:] + + widget.use_effect(update_data, dependencies or fig) + return fig_element + + +@widget.component +def Code(path, path_header=None): + path_header = path_header or path + with open(path) as f: + code = f.read() + md = widget.Markdown( + f""" +### {path_header} + +```python +{code} +``` + +""" + ) + + with v.ExpansionPanels() as main: + with v.ExpansionPanel(): + with v.ExpansionPanelHeader(children=["View source"]): + pass + with v.ExpansionPanelContent(children=[md]): + pass + return main diff --git a/nextpy/interfaces/jupyter/components/status/__init__.py b/nextpy/interfaces/jupyter/components/status/__init__.py new file mode 100644 index 00000000..1325cafd --- /dev/null +++ b/nextpy/interfaces/jupyter/components/status/__init__.py @@ -0,0 +1,3 @@ +from .alert import error, info, success, warning +from .progress import progress +from .spinner import spinner diff --git a/nextpy/interfaces/jupyter/components/status/alert.py b/nextpy/interfaces/jupyter/components/status/alert.py new file mode 100644 index 00000000..9fa9bdbe --- /dev/null +++ b/nextpy/interfaces/jupyter/components/status/alert.py @@ -0,0 +1,155 @@ +from typing import List, Optional, Union + +import reacton.ipyvuetify as v + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.util import _combine_classes + + +@widget.component +def success( + label: Optional[str] = None, + icon: Union[bool, str, None] = True, + dense=False, + outlined=True, + text=True, + children=[], + classes: List[str] = [], + **kwargs, +): + """Display a success message (green color). + + ## Arguments + + * `label`: the message to display + * `icon`: if True, display a check icon, if False, don't display an icon, if a string, + display the icon with that name ([Overview of available icons](https://pictogrammers.github.io/@mdi/font/4.9.95/)). + * `dense`: if True, display the message in a dense format, using less vertical height. + * `outlined`: if True (default), display the message in an outlined border, instead of a filled box. + * `text`: if True (default), display the message in a text format, which applies a semi-transparent background. + * `classes`: additional CSS classes to apply. + """ + # vuetify doesn't accept True, but is ok with None for a default icon + if icon is True: + icon = None + return v.Alert( + type="success", + text=text, + outlined=outlined, + dense=dense, + icon=icon, + children=[*([label] if label is not None else []), *children], + class_=_combine_classes(classes), + **kwargs, + ) + + +@widget.component +def info( + label: Optional[str] = None, + icon: Union[bool, str, None] = True, + dense=False, + outlined=True, + text=True, + children=[], + classes: List[str] = [], + **kwargs, +): + """Display a info message (blue color). + + ## Arguments + + * `label`: the message to display + * `icon`: if True, display a info icon, if False, don't display an icon, if a string, + display the icon with that name ([Overview of available icons](https://pictogrammers.github.io/@mdi/font/4.9.95/)). + * `dense`: if True, display the message in a dense format, using less vertical height. + * `outlined`: if True (default), display the message in an outlined border, instead of a filled box. + * `text`: if True (default), display the message in a text format, which applies a semi-transparent background. + * `classes`: additional CSS classes to apply. + """ + if icon is True: + icon = None + return v.Alert( + type="info", + text=text, + outlined=outlined, + dense=dense, + icon=icon, + children=[*([label] if label is not None else []), *children], + class_=_combine_classes(classes), + **kwargs, + ) + + +@widget.component +def warning( + label: Optional[str] = None, + icon: Union[bool, str, None] = True, + dense=False, + outlined=True, + text=True, + children=[], + classes: List[str] = [], + **kwargs, +): + """Display a warning message (orange color). + + ## Arguments + + * `label`: the message to display + * `icon`: if True, display a exclamation icon, if False, don't display an icon, if a string, + display the icon with that name ([Overview of available icons](https://pictogrammers.github.io/@mdi/font/4.9.95/)). + * `dense`: if True, display the message in a dense format, using less vertical height. + * `outlined`: if True (default), display the message in an outlined border, instead of a filled box. + * `text`: if True (default), display the message in a text format, which applies a semi-transparent background. + * `classes`: additional CSS classes to apply. + """ + if icon is True: + icon = None + return v.Alert( + type="warning", + text=text, + outlined=outlined, + dense=dense, + icon=icon, + children=[*([label] if label is not None else []), *children], + class_=_combine_classes(classes), + **kwargs, + ) + + +@widget.component +def error( + label: Optional[str] = None, + icon: Union[bool, str, None] = True, + dense=False, + outlined=True, + text=True, + children=[], + classes: List[str] = [], + **kwargs, +): + """Display an error message (red color). + + ## Arguments + + * `label`: the message to display + * `icon`: if True, display a exclamation in a red triangle icon, if False, don't display an icon, if a string, + display the icon with that name ([Overview of available icons](https://pictogrammers.github.io/@mdi/font/4.9.95/)). + * `dense`: if True, display the message in a dense format, using less vertical height. + * `outlined`: if True (default), display the message in an outlined border, instead of a filled box. + * `text`: if True (default), display the message in a text format, which applies a semi-transparent background. + * `classes`: additional CSS classes to apply. + """ + if icon is True: + icon = None + return v.Alert( + type="error", + text=text, + outlined=outlined, + dense=dense, + icon=icon, + children=[*([label] if label is not None else []), *children], + class_=_combine_classes(classes), + **kwargs, + ) diff --git a/nextpy/interfaces/jupyter/components/status/progress.py b/nextpy/interfaces/jupyter/components/status/progress.py new file mode 100644 index 00000000..11809857 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/status/progress.py @@ -0,0 +1,47 @@ +from typing import Dict, List, Union + +import reacton.ipyvuetify as v + +import nextpy.interfaces.jupyter as widget +import nextpy.interfaces.jupyter.util + + +@widget.component +def progress( + value: Union[bool, float] = True, + color="primary", + style: Union[str, Dict[str, str], None] = None, + classes: List[str] = [], +): + """Progress bar component showing a percentage, indeterminate or hidden. + + * When `value` is `True`, the progress bar will be indeterminate. + * When `value` is `False`, the progress bar will be hidden, but still take up space. + This can be used to avoid the page to jump when the progress bar is shown and hidden. + * When `value` is a number between 0 and 100, the progress bar will show the percentage. + + ### Basic example + + ```widget + import nextpy.interfaces.jupyter + + @widget.component + def Page(): + widget.ProgressLinear(value=30, color="purple") + widget.ProgressLinear(True) + ``` + + ## Arguments + * `value`: Value of the progress bar. Can be a number between 0 and 100, `True` for indeterminate or `False` to hide. + * `color`: Color of the progress bar. + * `style`: CSS style to add to the progress bar. + * `classes`: List of classes to apply to the progress bar. + + """ + indeterminate = value is True + + style_flat = widget.util._flatten_style(style) + if value is False: + style_flat = "visibility: hidden;" + style_flat + class_ = widget.util._combine_classes(classes) + v.ProgressLinear(indeterminate=indeterminate, value=value if value is not True else None, color=color, style_=style_flat, class_=class_) diff --git a/nextpy/interfaces/jupyter/components/status/spinner.py b/nextpy/interfaces/jupyter/components/status/spinner.py new file mode 100644 index 00000000..88987321 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/status/spinner.py @@ -0,0 +1,30 @@ +import ipyvue +import traitlets + +import nextpy.interfaces.jupyter as widget + + +class SpinnerSolaraWidget(ipyvue.VueTemplate): + template_file = (__file__, "spinner-widget.vue") + + size = traitlets.Unicode("64px").tag(sync=True) + + +@widget.component +def spinner(size="64px"): + """Spinner component with the Solara logo to indicate the app is busy. + + ### Basic example + + ```widget + import nextpy.interfaces.jupyter + + @widget.component + def Page(): + widget.SpinnerSolara(size="100px") + ``` + + ## Arguments + * `size`: Size of the spinner. + """ + return SpinnerSolaraWidget.element(size=size) diff --git a/nextpy/interfaces/jupyter/components/style.py b/nextpy/interfaces/jupyter/components/style.py new file mode 100644 index 00000000..e33ac271 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/style.py @@ -0,0 +1,105 @@ +import asyncio +import hashlib +import logging +from pathlib import Path +from typing import Optional, Union, cast + +import ipyvuetify as v + +try: + import watchfiles +except ModuleNotFoundError: + watchfiles = None # type: ignore + +import nextpy.interfaces.jupyter as widget +import nextpy.interfaces.jupyter.server.settings + +logger = logging.getLogger("widget.style") + + +@widget.component +def style(value: Union[str, Path] = ""): + """Add a custom piece of CSS. + + Note that this is considered an advanced feature, and should be used with caution. + + ## Example + + ```python + from pathlib import Path + import nextpy.interfaces.jupyter + + + @widget.component + def Page(): + widget.style(Path("style.css")) + widget.button("Click me", classes=["company-style-button"]) + ``` + + ## Arguments + + - `value`: The CSS string of CSS file to insert into the page. In development mode (the default) + when a Path argument is passed, the file will be watched for changes, and the CSS will be reloaded when + the file changes. In production mode, the CSS will be inserted once, and not reloaded. + """ + css_content_reloaded, set_css_content_reloaded = widget.use_state(cast(Optional[str], None)) + + def hot_reload(): + in_production = widget.server.settings.main.mode == "production" + if not in_production and isinstance(value, Path): + + async def watch(): + try: + async for _ in watchfiles.awatch(value): + print(value, "changed, reloading css") # noqa + set_css_content_reloaded(cast(Path, value).read_text()) + except RuntimeError: + pass # swallow the RuntimeError: Already borrowed errors from watchfiles + except Exception: + logger.exception("Error watching file") + + if watchfiles: + future = asyncio.create_task(watch()) + return future.cancel + else: + logger.warning("watchfiles not installed, cannot watch file") + + widget.use_effect(hot_reload, [value]) + + uuid = widget.use_unique_key() + if css_content_reloaded is not None: + css_content = css_content_reloaded + else: + css_content = value.read_text() if isinstance(value, Path) else value + # del value + hash = hashlib.sha256(css_content.encode("utf-8")).hexdigest() + # the key is unique for this component + value + # so that we create a new component if the value changes + # but we do not remove the css of a component with the same value + key = uuid + "-" + hash + # ipyvue does not remove the css itself, so we need to do it manually + script = ( + """ + + """ + % key + ) + + template = f""" + +{script} + + """ + # using .key avoids re-using the template, which causes a flicker (due to ipyvue) + return v.VuetifyTemplate.element(template=template).key(key) diff --git a/nextpy/interfaces/jupyter/components/typography/__init__.py b/nextpy/interfaces/jupyter/components/typography/__init__.py new file mode 100644 index 00000000..48c74e0f --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/__init__.py @@ -0,0 +1,9 @@ +from .caption import caption +from .code import code +from .divider import divider +from .header import header +from .headings import h1, h2, h3, h4, h5, h6 +from .markdown import markdown +from .text import text +from .title_file import title +from .tooltip import tooltip \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/typography/caption.py b/nextpy/interfaces/jupyter/components/typography/caption.py new file mode 100644 index 00000000..48e00af9 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/caption.py @@ -0,0 +1,20 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + +@widget.component +def caption( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + caption_style = 'font-size: 0.8em; font-style: italic; color: gray; margin-top: 10px; ' + return v.Html( + tag = 'h1', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=caption_style+style, + **kwargs + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/typography/code.py b/nextpy/interfaces/jupyter/components/typography/code.py new file mode 100644 index 00000000..8eb3bf22 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/code.py @@ -0,0 +1,26 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + +@widget.component +def code( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'pre', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) + +# class CodeHighlightCssWidget(v.VuetifyTemplate): +# template_file = (__file__, "code.vue") + +# @widget.component +# def code_highlight(): +# return CodeHighlightCssWidget.element() \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/typography/code.vue b/nextpy/interfaces/jupyter/components/typography/code.vue new file mode 100644 index 00000000..42b52847 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/code.vue @@ -0,0 +1,63 @@ + + + diff --git a/nextpy/interfaces/jupyter/components/typography/divider.py b/nextpy/interfaces/jupyter/components/typography/divider.py new file mode 100644 index 00000000..e0c83368 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/divider.py @@ -0,0 +1,19 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + +@widget.component +def divider( + children=[], + class_name: str = '', + style:str = '', + vertical: bool = None, + **kwargs, +): + return v.Divider( + children=[*children], + class_=class_name, + style_=style, + vertical=vertical, + **kwargs, + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/typography/header.py b/nextpy/interfaces/jupyter/components/typography/header.py new file mode 100644 index 00000000..1f830dbe --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/header.py @@ -0,0 +1,19 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + +@widget.component +def header( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'h1', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/typography/headings.py b/nextpy/interfaces/jupyter/components/typography/headings.py new file mode 100644 index 00000000..53dd1461 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/headings.py @@ -0,0 +1,101 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + +from nextpy.interfaces.jupyter.util import _combine_classes + +@widget.component +def h1( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'h1', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) + +@widget.component +def h2( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'h2', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) + +@widget.component +def h3( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'h3', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) + +@widget.component +def h4( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'h4', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) + +@widget.component +def h5( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'h5', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) + +@widget.component +def h6( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'h6', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/typography/markdown.py b/nextpy/interfaces/jupyter/components/typography/markdown.py new file mode 100644 index 00000000..1638ac9d --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/markdown.py @@ -0,0 +1,285 @@ +import hashlib +import html +import logging +import textwrap +import traceback +import warnings +from typing import Any, Dict, List, Union + +import ipyvuetify as v +import pymdownx.emoji +import pymdownx.highlight +import pymdownx.superfences + +import nextpy.interfaces.jupyter as widget +# import nextpy.interfaces.jupyter.components.applayout + +try: + import pygments + + has_pygments = True +except ModuleNotFoundError: + has_pygments = False +else: + from pygments.formatters import HtmlFormatter + from pygments.lexers import get_lexer_by_name + +logger = logging.getLogger(__name__) + +html_no_execute_enabled = "
Solara execution is not enabled
" + + +@widget.component +def ExceptionGuard(children=[]): + exception, clear_exception = widget.use_exception() + if exception: + widget.Error(f"Oops, an error occurred: {str(exception)}") + with widget.Details("Exception details"): + error = "".join(traceback.format_exception(None, exception, exception.__traceback__)) + widget.pre(error) + else: + if len(children) == 1: + return children[0] + else: + widget.column(children=children) + + +def _run_solara(code): + ast = compile(code, "markdown", "exec") + local_scope: Dict[Any, Any] = {} + exec(ast, local_scope) + app = None + if "app" in local_scope: + app = local_scope["app"] + elif "Page" in local_scope: + Page = local_scope["Page"] + app = widget.components.applayout._AppLayoutEmbed(children=[ExceptionGuard(children=[Page()])]) + else: + raise NameError("No Page of app defined") + box = v.Html(tag="div") + box, rc = widget.render(app, container=box) + widget_id = box._model_id + return ( + '
' + f'loading widget...' + '
Live output
' + ) + + +def _markdown_template(html, style=""): + return ( + """ + + + + """ + ) + + +def _highlight(src, language, unsafe_solara_execute, extra, *args, **kwargs): + """Highlight a block of code""" + + if not has_pygments: + warnings.warn("Pygments is not installed, code highlighting will not work, use pip install pygments to install it.") + src_safe = html.escape(src) + return f"
{src_safe}
" + + run_src_with_solara = False + if language == "widget": + run_src_with_solara = True + language = "python" + + lexer = get_lexer_by_name(language) + formatter = HtmlFormatter() + src_html = pygments.highlight(src, lexer, formatter) + + if run_src_with_solara: + if unsafe_solara_execute: + html_widget = _run_solara(src) + return src_html + html_widget + else: + return src_html + html_no_execute_enabled + else: + return src_html + + +# @widget.component +# def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: bool = False): +# md_text = textwrap.dedent(md_text) + +# from markdown_it import MarkdownIt as MarkdownItMod +# from mdit_py_plugins import container, deflist # noqa: F401 +# from mdit_py_plugins.footnote import footnote_plugin # noqa: F401 +# from mdit_py_plugins.front_matter import front_matter_plugin # noqa: F401 + +# def highlight_code(code, name, attrs): +# return _highlight(code, name, unsafe_solara_execute, attrs) + +# md = MarkdownItMod( +# "js-default", +# { +# "html": True, +# "typographer": True, +# "highlight": highlight_code, +# }, +# ) +# md = md.use(container.container_plugin, name="note") +# html = md.render(md_text) +# hash = hashlib.sha256((html + str(unsafe_solara_execute) + repr(highlight)).encode("utf-8")).hexdigest() +# return v.VuetifyTemplate.element(template=_markdown_template(html)).key(hash) + + +_index = pymdownx.emoji.emojione(None, None) + + +def _no_deep_copy_emojione(options, md): + return _index + + +@widget.component +def markdown(md_text: str, unsafe_allow_html=False, style: Union[str, Dict, None] = None): + """Renders markdown text + + Renders markdown using https://python-markdown.github.io/ + + Math rendering is done using Latex syntax, using https://www.mathjax.org/. + + ## Examples + + ### Basic + + ```widget + import nextpy.interfaces.jupyter + + + @widget.component + def Page(): + return widget.Markdown(r''' + # This is a title + + ## This is a subtitle + This is a markdown text, **bold** and *italic* text is supported. + + ## Math + Also, $x^2$ is rendered as math. + + Or multiline math: + $$ + \\int_0^1 x^2 dx = \\frac{1}{3} + $$ + + ''') + ``` + + ## Arguments + + * `md_text`: The markdown text to render + * `unsafe_solara_execute`: If True, code marked with language "widget" will be executed. This is potentially unsafe + if the markdown text can come from user input and should only be used for trusted markdown. + * `style`: A string or dict of css styles to apply to the rendered markdown. + + """ + import markdown + + unsafe_solara_execute = unsafe_allow_html + md_text = textwrap.dedent(md_text) + style = widget.util._flatten_style(style) + + def make_markdown_object(): + def highlight(src, language, *args, **kwargs): + try: + return _highlight(src, language, unsafe_solara_execute, *args, **kwargs) + except Exception as e: + logger.exception("Error highlighting code: %s", src) + return repr(e) + + return markdown.Markdown( # type: ignore + extensions=[ + "pymdownx.highlight", + "pymdownx.superfences", + "pymdownx.emoji", + "toc", # so we get anchors for h1 h2 etc + "tables", + ], + extension_configs={ + "pymdownx.emoji": { + "emoji_index": _no_deep_copy_emojione, + }, + "pymdownx.superfences": { + "custom_fences": [ + { + "name": "mermaid", + "class": "mermaid", + "format": pymdownx.superfences.fence_div_format, + }, + { + "name": "widget", + "class": "", + "format": highlight, + }, + ], + }, + }, + ) + + md = widget.use_memo(make_markdown_object, dependencies=[unsafe_solara_execute]) + html = md.convert(md_text) + # if we update the template value, the whole vue tree will rerender (ipvue/ipyvuetify issue) + # however, using the hash we simply generate a new widget each time + hash = hashlib.sha256((html + str(unsafe_solara_execute)).encode("utf-8")).hexdigest() + return v.VuetifyTemplate.element(template=_markdown_template(html, style)).key(hash) diff --git a/nextpy/interfaces/jupyter/components/typography/spinner-solara.vue b/nextpy/interfaces/jupyter/components/typography/spinner-solara.vue new file mode 100644 index 00000000..36edb4e4 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/spinner-solara.vue @@ -0,0 +1,105 @@ + + + diff --git a/nextpy/interfaces/jupyter/components/typography/subheader.py b/nextpy/interfaces/jupyter/components/typography/subheader.py new file mode 100644 index 00000000..c07ff238 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/subheader.py @@ -0,0 +1,19 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + +@widget.component +def header( + label: Optional[str] = None, + children=[], + class_name: str = '', + style: str = '', + **kwargs +): + return v.Html( + tag = 'h2', + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/typography/text.py b/nextpy/interfaces/jupyter/components/typography/text.py new file mode 100644 index 00000000..ad624513 --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/text.py @@ -0,0 +1,20 @@ +import reacton.ipyvuetify as v +from typing import List, Optional, Union +import nextpy.interfaces.jupyter as widget + +# from nextpy.interfaces.jupyter.util import _combine_classes + +@widget.component +def text( + label: Optional[str] = None, + children: List = [], + class_name: str = '', + style:str = '', + **kwargs, +): + return v.Text( + children=[*([label] if label is not None else []), *children], + class_=class_name, + style_=style, + **kwargs, + ) \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/components/typography/title.vue b/nextpy/interfaces/jupyter/components/typography/title.vue new file mode 100644 index 00000000..b390e11d --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/title.vue @@ -0,0 +1,38 @@ + + + diff --git a/nextpy/interfaces/jupyter/components/typography/title_file.py b/nextpy/interfaces/jupyter/components/typography/title_file.py new file mode 100644 index 00000000..4f4989ef --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/title_file.py @@ -0,0 +1,90 @@ +from typing import Callable, Dict, Optional, Tuple, cast + +import ipyvuetify as vy +import reacton.core +import traitlets + +import nextpy.interfaces.jupyter as jupyter + + + +class TitleWidget(vy.VuetifyTemplate): + template_file = (__file__, "title.vue") + title = traitlets.Unicode().tag(sync=True) + level = traitlets.Int().tag(sync=True) + + +Titles = Dict[str, Tuple[int, str]] + + +def _set_titles_default(updater: Callable[[Titles], Titles]): + pass + + +titles_context = jupyter.create_context(_set_titles_default) + + +def use_title_get() -> Optional[str]: + titles, set_titles = jupyter.use_state(cast(Titles, {})) + titles_context.provide(set_titles) # type: ignore + if titles: + title = max([(order, title) for (key, (order, title)) in titles.items()], key=lambda x: x[0])[1] + else: + title = None + return title + + +def use_title_set(title: str, offset: int): + key = jupyter.use_unique_key(prefix="title-") + set_titles = jupyter.use_context(titles_context) + + def update(): + set_titles(lambda titles: {**titles, key: (offset, title)}) + + jupyter.use_effect(update, [title]) + + def restore(): + def cleanup(): + def without(titles): + titles_restored = titles.copy() + titles_restored.pop(key, None) + return titles_restored + + set_titles(without) + + return cleanup + + jupyter.use_effect(restore, []) + + +@jupyter.component +def title(title: str): + """Set the title of a page. + + ```python + import jupyter + + @jupyter.component + def Page(): + with jupyter.vstack() as main: + MyAwesomeComponent() + jupyter.Title("My page title") + return main + ``` + + If multiple Title components are used, the 'deepest' child will take precedence. + + ## Arguments + + * title: the title of the page + """ + level = 0 + rc = reacton.core.get_render_context() + context = rc.context + while context and context.parent: + level += 1 + context = context.parent + offset = 2**level + use_title_set(title, offset) + + return TitleWidget.element(title=title, level=level) diff --git a/nextpy/interfaces/jupyter/components/typography/tooltip.py b/nextpy/interfaces/jupyter/components/typography/tooltip.py new file mode 100644 index 00000000..43fdf22e --- /dev/null +++ b/nextpy/interfaces/jupyter/components/typography/tooltip.py @@ -0,0 +1,61 @@ +from typing import Optional, Union + +import reacton.ipyvuetify as v + +import nextpy.interfaces.jupyter as widget + + +@widget.component +def tooltip( + tooltip=Union[str, widget.Element], + children=[], + color: Optional[str] = None, +): + """A tooltip that is shown when you hover above an element. + + Not all components support tooltips, in case it does not work, + try wrapping the element in a `column` or `row`. + + ```widget + import nextpy.interfaces.jupyter + + @widget.component + def Page(): + with widget.Tooltip("This is a tooltip over a button"): + widget.button("Hover me") + with widget.Tooltip("This is a tooltip over a text"): + widget.text("Hover me") + info = widget.Info("Any component is supported as tooltip.") + with widget.Tooltip(info, color="white"): + with widget.column(): + widget.Markdown("# Lorem ipsum\\n\\nDolor sit amet") + ``` + + ## Arguments + + * `tooltip`: the text, or element to display on hover. + * `children`: the element to display the tooltip over. + * `color`: the color of the tooltip (if None, the default color). + + """ + + def set_v_on(): + for child in children: + widget = widget.get_widget(child) + # this only works on vue/vuetify components + widget.v_on = "tooltip.on" # type: ignore + + widget.use_effect(set_v_on, children) + + return v.Tooltip( + bottom=True, + v_slots=[ + { + "name": "activator", + "variable": "tooltip", + "children": children, + } + ], + color=color, + children=[tooltip], + ) diff --git a/nextpy/interfaces/jupyter/datatypes.py b/nextpy/interfaces/jupyter/datatypes.py new file mode 100644 index 00000000..54b9da1d --- /dev/null +++ b/nextpy/interfaces/jupyter/datatypes.py @@ -0,0 +1,143 @@ +import dataclasses +from enum import Enum +from pathlib import Path +from types import ModuleType +from typing import Any, Callable, Dict, Generic, List, Optional, TypeVar, Union + +import reacton +from typing_extensions import Literal, TypedDict + +T = TypeVar("T") +U = TypeVar("U") + + +@dataclasses.dataclass(frozen=True) +class Action: + name: str + icon: Optional[str] = None + on_click: Optional[Callable] = None + + +@dataclasses.dataclass(frozen=True) +class columnAction(Action): + on_click: Optional[Callable[[str], None]] = None + + +@dataclasses.dataclass(frozen=True) +class CellAction(Action): + on_click: Optional[Callable[[str, int], None]] = None + + +def _fallback_retry(): + raise RuntimeError("Should not happen") + + +class ResultState(Enum): + INITIAL = 1 + STARTING = 2 + WAITING = 3 + RUNNING = 4 + ERROR = 5 + FINISHED = 6 + CANCELLED = 7 + + +@dataclasses.dataclass(frozen=True) +class Result(Generic[T]): + value: Optional[T] = None + error: Optional[Exception] = None + state: ResultState = ResultState.INITIAL + progress: Optional[float] = None + + def retry(self): + # mypy does not like members that are callable + # gets confused about self argument. + # we wrap it to avoid hitting this error in user + # code + self._retry() # type: ignore + + # can we avoid storing these into the dataclass? + _retry: Callable[[], Any] = dataclasses.field(compare=False, default=lambda: None) + cancel: Callable[[], Any] = dataclasses.field(compare=False, default=lambda: None) + + def __or__(self, next: Callable[["Result[T]"], "Result[U]"]): + return next(self) + + +@dataclasses.dataclass(frozen=True) +class FileContentResult(Result[T]): + @property + def exists(self): + return not isinstance(self.error, FileNotFoundError) + + +class AggregationCount(TypedDict): + type: Literal["count"] + + +class AggregationSum(TypedDict): + type: Literal["sum"] + + +JsonType = Union[None, int, str, bool, List[Any], Dict[str, Any]] + +Aggregation = Union[AggregationCount, AggregationSum] + + +class PivotTableData(TypedDict): + x: List[str] + y: List[str] + agg: str + values: List[List[JsonType]] + values_x: List[str] + values_y: List[str] + headers_x: List[List[str]] + headers_y: List[List[str]] + counts_x: int + counts_y: int + total: str + + +@dataclasses.dataclass(frozen=True) +class Route: + """A route tells Solara which component to render for a given URL. (Not a Solara component!) + + ## Arguments + + * `path` - The path of the route. This is the part of the URL that you see in the browser. + * `children` - (Optional) A list of child routes. These are routes that are nested under this route. + * `module` - (Optional) The module that contains the component to render for this route, used for autorouting. + * `component` - (Optional) The component to render for this route. + * `layout` - (Optional) The layout to use for this route. If not specified, the default layout will be used. + * `data` - (Optional) The data to pass to the component for this route, usage is up to the user. + * `label` - (Optional) The label to use for this route, can be used for labeling tabs or links. + + ## See also + + * [Multipage](/docs/howto/multipage). + * [Understanding Routing](/docs/understanding/routing). + + """ + + path: str + children: List["Route"] = dataclasses.field(default_factory=list) + + # these are free to use, depending on the implementation + # see autorouting.py for how Solara uses them + module: Optional[ModuleType] = None + + # in the autorouting implementation, this is the + # the same as module.Page (unless we are rendering a markdown) + component: Union[None, Callable, reacton.core.Component] = None + layout: Union[None, Callable, reacton.core.Component] = None + + # in the autorouting implementation, this is the + # path of the markdown file + data: Any = None + + # Can be used for a title and/or a tab label + label: Optional[str] = None + + # file corresponding to this route, can be used for + # checking of content has changed (using mtime) + file: Optional[Path] = None diff --git a/nextpy/interfaces/jupyter/express.py b/nextpy/interfaces/jupyter/express.py new file mode 100644 index 00000000..2abe1c17 --- /dev/null +++ b/nextpy/interfaces/jupyter/express.py @@ -0,0 +1,237 @@ +import functools +import typing +from typing import Callable + +import numpy as np +import plotly.express as px +import plotly.graph_objs as go +import typing_extensions + +# behave as the px module +from plotly.express import * # noqa: F401, F403 +from plotly.express._core import make_figure + +import nextpy.interfaces.jupyter as widget + +P = typing_extensions.ParamSpec("P") +T = typing.TypeVar("T") +T2 = typing.TypeVar("T2") + + +# patch histogram until issue 3859 in plotly is resolved + + +def histogram( + data_frame=None, + x=None, + y=None, + custom_data=None, + color=None, + pattern_shape=None, + facet_row=None, + facet_col=None, + facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, + hover_name=None, + hover_data=None, + animation_frame=None, + animation_group=None, + category_orders=None, + labels=None, + color_discrete_sequence=None, + color_discrete_map=None, + pattern_shape_sequence=None, + pattern_shape_map=None, + marginal=None, + opacity=None, + orientation=None, + barmode="relative", + barnorm=None, + histnorm=None, + log_x=False, + log_y=False, + range_x=None, + range_y=None, + histfunc=None, + cumulative=None, + nbins=None, + text_auto=False, + title=None, + template=None, + width=None, + height=None, +) -> go.Figure: + """ + In a histogram, rows of `data_frame` are grouped together into a + rectangular mark to visualize the 1D distribution of an aggregate + function `histfunc` (e.g. the count or sum) of the value `y` (or `x` if + `orientation` is `'h'`). + """ + return make_figure( + args=locals(), + constructor=go.Histogram, + trace_patch=dict( + histnorm=histnorm, + histfunc=histfunc, + cumulative=dict(enabled=cumulative), + ), + layout_patch=dict(barmode=barmode, barnorm=barnorm), + ) + + +px.histogram = histogram + + +def _make_self_describing(f: Callable[P, T]): + # put the arguments on the function and the function into the return value + @functools.wraps(f) + def wrapper(*args: P.args, **kwargs: P.kwargs): + fig = f(*args, **kwargs) + fig._func = f # type: ignore + fig._args = args # type: ignore + fig._kwargs = kwargs # type: ignore + return fig + + return wrapper + + +def _patch(): + for name in dir(px): + f = getattr(px, name) + if callable(f): + f = _make_self_describing(f) + setattr(px, name, f) + + +_patch() + + +@widget.component +def CrossFilteredFigurePlotly(fig): + first_arg_name = fig._func.__code__.co_varnames[0] + kwargs = fig._kwargs.copy() + kwargs.update(zip(fig._func.__code__.co_varnames, fig._args)) + + df = kwargs[first_arg_name] + name = fig._func.__name__ + filter, set_filter = widget.use_cross_filter(id(df), name) + dff = df + if filter is not None: + dff = df[filter] + for key, value in kwargs.items(): + if not isinstance(value, str): + try: + n = len(value) + except TypeError: + pass # int or bool, or anything without a length + except Exception: + raise + else: + if n == len(df): + kwargs[key] = np.array(value)[filter] + original_indices = np.arange(len(df), dtype="int64") + else: + original_indices = None + + kwargs[first_arg_name] = dff + + index = np.arange(len(dff)) + custom_data = [index] + has_custom_data = "custom_data" in kwargs + + if has_custom_data: + kwargs["custom_data"] = custom_data + list(kwargs["custom_data"]) + else: + kwargs["custom_data"] = custom_data + new_fig = fig._func(**kwargs) + new_fig.layout = fig.layout + + # the df is split into multiple traces, generate the data such that we can transform back from trace+point indices to df.index + indices = [] + offset = 0 + index_offsets_list = [] + for data in new_fig.data: + indices.append(data.customdata.T[0]) + index_offsets_list.append(offset) + offset += len(data.customdata.T[0]) + index_offsets = np.array(index_offsets_list) + # strip of the custom data we added + for data in new_fig.data: + if has_custom_data: + data.customdata = data.customdata.T[len(custom_data) :].T + else: + data.customdata = None + + def on_selection(data): + if data is not None: + trace_indexes = np.array(data["points"]["trace_indexes"]) + point_indexes = np.array(data["points"]["point_indexes"]) + if len(trace_indexes): + indices_selected = index_offsets[trace_indexes] + point_indexes + + if filter is not None: + assert original_indices is not None + # these are references to the filtered dataframe + indices_selected = original_indices[filter][indices_selected] + mask = np.zeros(len(df), dtype=bool) + mask[indices_selected] = True + set_filter(mask) + else: + set_filter(None) + + def on_deselect(data): + set_filter(None) + + return widget.FigurePlotly(new_fig, on_selection=on_selection, on_deselect=on_deselect) + + +# for backwards compatibility +FigurePlotlyCrossFiltered = CrossFilteredFigurePlotly + + +def _wraps(f: Callable[P, T]): + @functools.wraps(f) + def wrapper(*args: P.args, **kwargs: P.kwargs): + fig = f(*args, **kwargs) + return CrossFilteredFigurePlotly(fig) + + return wrapper + + +scatter = _wraps(px.scatter) +scatter_3d = _wraps(px.scatter_3d) +scatter_polar = _wraps(px.scatter_polar) +scatter_ternary = _wraps(px.scatter_ternary) +scatter_mapbox = _wraps(px.scatter_mapbox) +scatter_geo = _wraps(px.scatter_geo) +scatter_matrix = _wraps(px.scatter_matrix) +density_contour = _wraps(px.density_contour) +density_heatmap = _wraps(px.density_heatmap) +density_mapbox = _wraps(px.density_mapbox) +line = _wraps(px.line) +line_3d = _wraps(px.line_3d) +line_polar = _wraps(px.line_polar) +line_ternary = _wraps(px.line_ternary) +line_mapbox = _wraps(px.line_mapbox) +line_geo = _wraps(px.line_geo) +parallel_coordinates = _wraps(px.parallel_coordinates) +parallel_categories = _wraps(px.parallel_categories) +area = _wraps(px.area) +bar = _wraps(px.bar) +timeline = _wraps(px.timeline) +bar_polar = _wraps(px.bar_polar) +violin = _wraps(px.violin) +box = _wraps(px.box) +strip = _wraps(px.strip) +histogram = _wraps(px.histogram) +ecdf = _wraps(px.ecdf) +choropleth = _wraps(px.choropleth) +choropleth_mapbox = _wraps(px.choropleth_mapbox) +pie = _wraps(px.pie) +sunburst = _wraps(px.sunburst) +treemap = _wraps(px.treemap) +icicle = _wraps(px.icicle) +funnel = _wraps(px.funnel) +funnel_area = _wraps(px.funnel_area) +imshow = _wraps(px.imshow) diff --git a/nextpy/interfaces/jupyter/hooks/__init__.py b/nextpy/interfaces/jupyter/hooks/__init__.py new file mode 100644 index 00000000..27c80db3 --- /dev/null +++ b/nextpy/interfaces/jupyter/hooks/__init__.py @@ -0,0 +1,3 @@ +from .misc import * # noqa: #F401 F403 +from .use_reactive import use_reactive # noqa: #F401 F403 +from .use_thread import use_thread # noqa: #F401 F403 \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/hooks/misc.py b/nextpy/interfaces/jupyter/hooks/misc.py new file mode 100644 index 00000000..f0aea18c --- /dev/null +++ b/nextpy/interfaces/jupyter/hooks/misc.py @@ -0,0 +1,234 @@ +import dataclasses +import io +import json +import logging +import os + +# import tempfile +import threading +import time +import urllib.request +import uuid +from typing import IO, Any, Callable, Tuple, TypeVar, Union, cast + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.datatypes import FileContentResult, Result + +logger = logging.getLogger("react-ipywidgets.extra.hooks") +chunk_size_default = 1024**2 + +__all__ = [ + "use_download", + "use_fetch", + "use_json_load", + "use_json", + "use_file_content", + "use_uuid4", + "use_unique_key", + "use_state_or_update", + "use_previous", +] +T = TypeVar("T") +U = TypeVar("U") + +MaybeResult = Union[T, Result[T]] + + +def use_retry(*actions: Callable[[], Any]): + counter, set_counter = widget.use_state(0) + + def retry(): + for action in actions: + action() + set_counter(lambda counter: counter + 1) + + return counter, retry + + +def use_download( + f: MaybeResult[Union[str, os.PathLike, IO]], url, expected_size=None, delay=None, return_content=False, chunk_size=chunk_size_default +) -> Result: + from .use_thread import use_thread + + if not isinstance(f, Result): + f = Result(value=f) + assert isinstance(f, Result) + content_length, set_content_length = widget.use_state(expected_size, key="content_length") + downloaded_length = 0 + file_object = hasattr(f.value, "tell") + if not file_object: + file_path = cast(Union[str, os.PathLike], f.value) + if os.path.exists(file_path) and expected_size is not None: + file_size = os.path.getsize(file_path) + if file_size == expected_size: + downloaded_length = file_size + downloaded_length, set_downloaded_length = widget.use_state(downloaded_length, key="downloaded_length") + + def download(cancel: threading.Event): + assert isinstance(f, Result) + nonlocal downloaded_length + if expected_size is not None and downloaded_length == expected_size: + return # we already downloaded, but hooks cannot be conditional + + context: Any = None + if file_object: + context = widget.util.nullcontext() + output_file = cast(IO, f.value) + else: + # f = cast(Result[Union[str, os.PathLike]], f) + output_file = context = open(f.value, "wb") # type: ignore + + with context: + with urllib.request.urlopen(url) as response: + content_length = int(response.info()["Content-Length"]) + logger.info("content_length for %r = %r", url, content_length) + set_content_length(content_length) + bytes_read = 0 + while not cancel.is_set(): + chunk = response.read(chunk_size) + + if delay: + time.sleep(delay) + if not chunk: + break + bytes_read += len(chunk) + output_file.write(chunk) + set_downloaded_length(bytes_read) + return bytes_read + + result: Result[Any] = use_thread(download, [f, url]) + # maybe we wanna check this + # download_is_done = downloaded_length == content_length + + if content_length is not None: + progress = downloaded_length / content_length + else: + progress = 0 + + return dataclasses.replace(result, progress=progress) + + +def use_fetch(url, chunk_size=chunk_size_default): + # re-use the same file like object + f = widget.use_memo(io.BytesIO, [url]) + result = use_download(f, url, return_content=True, chunk_size=chunk_size) + return dataclasses.replace(result, value=f.getvalue() if result.progress == 1 else None) + + +def compose_result(head, *tail): + return head + + +def ensure_result(input: MaybeResult[T]) -> Result[T]: + if isinstance(input, Result): + return input + else: + return Result(value=input) + + +def make_use_thread(f: Callable[[T], U]): + from .use_thread import use_thread + + def use_result(input: MaybeResult[T]) -> Result[U]: + input_result = ensure_result(input) + + def in_thread(cancel: threading.Event): + if input_result.value: + return f(input_result.value) + + return use_thread(in_thread, dependencies=[input_result.value]) + + return use_result + + +@make_use_thread +def use_json_load(value: bytes): + return json.loads(value) + + +def use_json(path): + return use_fetch(path) | use_json_load + + +def use_file_content(path, watch=False) -> FileContentResult[bytes]: + counter, retry = use_retry() + + def read_file(*ignore): + try: + with open(path, "rb") as f: + return f.read() + except Exception as e: + return e + + result = None + try: + mtime = os.path.getmtime(path) + except Exception: + mtime = None + + content = widget.use_memo(read_file, dependencies=[path, mtime, counter]) + if result is not None: + return result + if isinstance(content, Exception): + return FileContentResult[bytes](error=content, _retry=retry) + else: + return FileContentResult[bytes](value=content, _retry=retry) + + +def use_force_update() -> Callable[[], None]: + """Returns a callable that can be used to force an update of a component. + + This is used when external state has change, and we need to re-render out component. + """ + _counter, set_counter = widget.use_state(0, "force update counter") + + def updater(): + set_counter(lambda count: count + 1) + + return updater + + +def use_uuid4(dependencies=[]) -> str: + """Generate a unique string using the uuid4 algorithm. Will only change when the dependencies change.""" + + def make_uuid(*_ignore): + return str(uuid.uuid4()) + + return widget.use_memo(make_uuid, dependencies) + + +def use_unique_key(key: str = None, prefix: str = "", dependencies=[]) -> str: + """Generate a unique string, or use key when not None. Dependencies are forwarded to `use_uuid4`.""" + uuid = use_uuid4(dependencies=dependencies) + return prefix + (key or uuid) + + +def use_state_or_update( + initial_or_updated: T, key: str = None, eq: Callable[[Any, Any], bool] = None +) -> Tuple[T, Callable[[Union[T, Callable[[T], T]]], None]]: + """This is useful for situations where a prop can change from a parent + component, which should be respected, and otherwise the internal + state should be kept. + """ + value, set_value = widget.use_state(initial_or_updated, key=key, eq=eq) + + def possibly_update(): + nonlocal value + # only gets called when initial_or_updated changes + set_value(initial_or_updated) + # this make sure the return value gets updated directly + value = initial_or_updated + + widget.use_memo(possibly_update, [initial_or_updated]) + return value, set_value + + +def use_previous(value: T, condition=True) -> T: + ref = widget.use_ref(value) + + def assign(): + if condition: + ref.current = value + + widget.use_effect(assign, [value]) + return ref.current diff --git a/nextpy/interfaces/jupyter/hooks/use_reactive.py b/nextpy/interfaces/jupyter/hooks/use_reactive.py new file mode 100644 index 00000000..2e519fdf --- /dev/null +++ b/nextpy/interfaces/jupyter/hooks/use_reactive.py @@ -0,0 +1,116 @@ +from typing import Callable, Optional, TypeVar, Union + +import nextpy.interfaces.jupyter as widget + +T = TypeVar("T") + + +def use_reactive( + value: Union[T, widget.Reactive[T]], + on_change: Optional[Callable[[T], None]] = None, +) -> widget.Reactive[T]: + """Creates a reactive variable with the a local component scope. + + It is a useful alternative to `use_state` when you want to use a + reactive variable for the component state. + See also [our documentation on state management](/docs/fundamentals/state-management). + + If the variable passed is a reactive variable, it will be returned instead and no + new reactive variable will be created. This is useful for implementing component + that accept either a reactive variable or a normal value along with an optional `on_change` + callback. + + ## Arguments: + + * value (Union[T, widget.Reactive[T]]): The value of the + reactive variable. If a reactive variable is provided, it will be + used directly. Otherwise, a new reactive variable will be created + with the provided initial value. If the argument passed changes + the reactive variable will be updated. + + * on_change (Optional[Callable[[T], None]]): An optional callback function + that will be called when the reactive variable's value changes. + + Returns: + widget.Reactive[T]: A reactive variable with the specified initial value + or the provided reactive variable. + + ## Examples + + ### Replacement for use_state + ```widget + import nextpy.interfaces.jupyter + + @widget.component + def ReusableComponent(): + color = widget.use_reactive("red") # another possibility + widget.Select(label="Color",values=["red", "green", "blue", "orange"], + value=color) + widget.Markdown("### Solara is awesome", style={"color": color.value}) + + @widget.component + def Page(): + # this component is used twice, but each instance has its own state + ReusableComponent() + ReusableComponent() + + ``` + + ### Flexible arguments + + The `MyComponent` component can be passed a reactive variable or a normal + Python variable and a `on_value` callback. + + ```python + import nextpy.interfaces.jupyter + from typing import Union, Optional, Callable + + @widget.component + def MyComponent(value: Union[T, widget.Reactive[T]], + on_value: Optional[Callable[[T], None]] = None, + ): + reactive_value = widget.use_reactive(value, on_value) + # Use the `reactive_value` in the component + ``` + """ + + try: + on_change_ref = widget.use_ref(on_change) + except RuntimeError as e: + raise RuntimeError( + "use_reactive must be called from a component function, inside the render function.\n" + "Do not call it top level, use [widget.reactive()](https://widget.dev/api/reactive) instead." + ) from e + on_change_ref.current = on_change + + def create(): + if not isinstance(value, widget.Reactive): + return widget.reactive(value) + + reactive_value = widget.use_memo(create, dependencies=[]) + if isinstance(value, widget.Reactive): + reactive_value = value + assert reactive_value is not None + updating = widget.use_ref(False) + + def forward_on_change(): + def forward(value): + if on_change_ref.current and not updating.current: + on_change_ref.current(value) + + return reactive_value.subscribe(forward) + + def update(): + updating.current = True + try: + if not isinstance(value, widget.Reactive): + reactive_value.value = value + finally: + updating.current = False + + widget.use_memo(update, [value]) + # if value is a reactive variable, and it changes, we need to subscribe to the latest + # reactive variable, otherwise we only link to it once + widget.use_effect(forward_on_change, [value] if isinstance(value, widget.Reactive) else []) + + return reactive_value diff --git a/nextpy/interfaces/jupyter/hooks/use_thread.py b/nextpy/interfaces/jupyter/hooks/use_thread.py new file mode 100644 index 00000000..dcf1e0fe --- /dev/null +++ b/nextpy/interfaces/jupyter/hooks/use_thread.py @@ -0,0 +1,129 @@ +import functools +import inspect +import logging +import os +import threading +from typing import Callable, Iterator, Optional, TypeVar, Union, cast + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter.datatypes import Result, ResultState +from nextpy.interfaces.jupyter.util import cancel_guard, nullcontext + +SOLARA_ALLOW_OTHER_TRACER = os.environ.get("SOLARA_ALLOW_OTHER_TRACER", False) in (True, "True", "true", "1") +T = TypeVar("T") +logger = logging.getLogger("widget.hooks.use_thread") + + +def use_thread( + callback=Union[ + Callable[[threading.Event], T], + Iterator[Callable[[threading.Event], T]], + Callable[[], T], + Iterator[Callable[[], T]], + ], + dependencies=[], + intrusive_cancel=True, +) -> Result[T]: + from .misc import use_force_update, use_retry + + def make_event(*_ignore_dependencies): + return threading.Event() + + def make_lock(): + return threading.Lock() + + lock: threading.Lock = widget.use_memo(make_lock, []) + updater = use_force_update() + result_state, set_result_state = widget.use_state(ResultState.INITIAL) + error = widget.use_ref(cast(Optional[Exception], None)) + result = widget.use_ref(cast(Optional[T], None)) + running_thread = widget.use_ref(cast(Optional[threading.Thread], None)) + counter, retry = use_retry() + cancel: threading.Event = widget.use_memo(make_event, [*dependencies, counter]) + + def run(): + set_result_state(ResultState.STARTING) + + def runner(): + wait_for_thread = None + with lock: + # if there is a current thread already, we'll need + # to wait for it. copy the ref, and set ourselves + # as the current one + if running_thread.current: + wait_for_thread = running_thread.current + running_thread.current = threading.current_thread() + if wait_for_thread is not None: + set_result_state(ResultState.WAITING) + # don't start before the previous is stopped + try: + wait_for_thread.join() + except: # noqa + pass + if threading.current_thread() != running_thread.current: + # in case a new thread was started that also was waiting for the previous + # thread to st stop, we can finish this + return + # we previously set current to None, but if we do not do that, we can still render the old value + # while we can still show a loading indicator using the .state + # result.current = None + set_result_state(ResultState.RUNNING) + + sig = inspect.signature(callback) + if sig.parameters: + f = functools.partial(callback, cancel) + else: + f = callback + try: + try: + # we only use the cancel_guard context manager around + # the function calls to f. We don't want to guard around + # a call to react, since that might slow down rendering + # during rendering + with cancel_guard(cancel) if intrusive_cancel else nullcontext(): + value = f() + if inspect.isgenerator(value): + while True: + try: + with cancel_guard(cancel) if intrusive_cancel else nullcontext(): + result.current = next(value) + error.current = None + except StopIteration: + break + # assigning to the ref doesn't trigger a rerender, so do it manually + updater() + if threading.current_thread() == running_thread.current: + set_result_state(ResultState.FINISHED) + else: + result.current = value + error.current = None + if threading.current_thread() == running_thread.current: + set_result_state(ResultState.FINISHED) + except Exception as e: + error.current = e + if threading.current_thread() == running_thread.current: + logger.exception(e) + set_result_state(ResultState.ERROR) + return + except widget.util.CancelledError: + pass + # this means this thread is cancelled not be request, but because + # a new thread is running, we can ignore this + finally: + if threading.current_thread() == running_thread.current: + running_thread.current = None + logger.info("thread done!") + if cancel.is_set(): + set_result_state(ResultState.CANCELLED) + + logger.info("starting thread: %r", runner) + thread = threading.Thread(target=runner, daemon=True) + thread.start() + + def cleanup(): + cancel.set() # cleanup for use effect + + return cleanup + + widget.use_effect(run, dependencies + [counter]) + return Result[T](value=result.current, error=error.current, state=result_state, cancel=cancel.set, _retry=retry) diff --git a/nextpy/interfaces/jupyter/minisettings.py b/nextpy/interfaces/jupyter/minisettings.py new file mode 100644 index 00000000..7409ba71 --- /dev/null +++ b/nextpy/interfaces/jupyter/minisettings.py @@ -0,0 +1,133 @@ +import os +from pathlib import Path +from typing import Any, Optional + +# similar API to pydantic/pydantic-settings but we prefer not to have a dependency on pydantic +# since we cannot be compatible with pydantic1 and 2 +# NOTE: not a public api + + +def _get_type(annotation): + check_optional_types = [str, int, float, bool, dict, list] + for check_type in check_optional_types: + if annotation == Optional[check_type]: + return check_type + if hasattr(annotation, "__origin__"): + if annotation.__origin__ == dict: + return dict + return annotation + + +class _Field: + def __init__(self, default=None, env=None, title=None, default_factory=None, gt=None, alias=None) -> None: + self.default = default + self.env = env + self.fullenv = None + self.title = title + self.annotation = None + self.default_factory = default_factory + self.gt = gt + self.alias = alias + self.field_info = self + self.extra = {"env_names": [env] if env else []} + + def __set_name__(self, owner, name): + prefix = "SOLARA_" + config = getattr(owner, "Config") + if config: + prefix = getattr(config, "env_prefix", prefix).upper() + if hasattr(config, "fields"): + fields = config.fields + if name in fields: + self.alias = fields[name] + self.name = name + self.alias = self.alias or self.name + self.title = self.title or self.name + if self.env is None: + self.env = f"{prefix}{self.name.upper()}" + else: + self.env = self.env + self.annotation = owner.__annotations__.get(self.name) + assert self.annotation is not None, f"Field {self.name} must have a type annotation" + self.type_ = _get_type(self.annotation) + + def __get__(self, instance, owner): + if instance is None: + return self + return instance._values[self.name] + + def __set__(self, instance, value): + instance._values[self.name] = value + + +def convert(annotation, value: str) -> Any: + check_optional_types = [str, int, float, bool, Path] + for check_type in check_optional_types: + if annotation == Optional[check_type]: + annotation = check_type + return convert(annotation, value) + if annotation == str: + return value + elif annotation == int: + return int(value) + elif annotation == float: + return float(value) + elif annotation == bool: + if value in ("True", "true", "1"): + return True + elif value in ("False", "false", "0"): + return False + else: + raise ValueError(f"Invalid boolean value {value}") + else: + # raise TypeError(f"Unsupported type {annotation}") + return annotation(value) + + +def Field(*args, **kwargs) -> Any: + return _Field(*args, **kwargs) + + +class BaseSettings: + __fields__: dict + + def __init__(self, **kwargs) -> None: + cls = type(self) + self._values = {**kwargs} + keys = {k.upper() for k in os.environ.keys()} + for key, field in cls.__dict__.items(): + if key in kwargs: + continue + if isinstance(field, _Field): + value = field.default + if field.default_factory: + value = field.default_factory() + + if field.env: + env_key = field.env.upper() + if env_key in keys: + # do a case-insensitive lookup + for env_var_cased in os.environ.keys(): + if env_key.upper() == env_var_cased.upper(): + value = convert(field.annotation, os.environ[env_var_cased]) + self._values[key] = value + + def __init_subclass__(cls) -> None: + cls.__fields__ = {} + for key, field in cls.__dict__.items(): + if key.startswith("_"): + continue + if key == "Config": + continue + if not isinstance(field, _Field): + field = Field(field) + setattr(cls, key, field) + field.__set_name__(cls, key) + cls.__fields__[key] = field + + def dict(self, by_alias=True): + values = self._values.copy() + for key, value in values.items(): + if isinstance(value, BaseSettings): + values[key] = value.dict(by_alias=by_alias) + return values diff --git a/nextpy/interfaces/jupyter/reactive.py b/nextpy/interfaces/jupyter/reactive.py new file mode 100644 index 00000000..fdedf25f --- /dev/null +++ b/nextpy/interfaces/jupyter/reactive.py @@ -0,0 +1,93 @@ +from typing import TypeVar + +from nextpy.interfaces.jupyter.toestand import Reactive + +__all__ = ["reactive", "Reactive"] + +T = TypeVar("T") + + +def reactive(value: T) -> Reactive[T]: + """Creates a new Reactive object with the given initial value. + + Reactive objects are mostly used to manage global or application-wide state in + Solara web applications. They provide an easy-to-use mechanism for keeping + track of the changing state of data and for propagating those changes to + the appropriate UI components. For managing local or component-specific + state, consider using the [`widget.use_state()`](/api/use_state) function. + + + Reactive variables can be accessed using the `.value` attribute. To modify + the value, you can either set the `.value` property directly or use the + `.set()` method. While both approaches are equivalent, the `.set()` method + is particularly useful when you need to pass it as a callback function to + other components, such as a slider's `on_value` callback. + + When a component uses a reactive variable, it + automatically listens for changes to the variable's value. If the value + changes, the component will automatically re-render to reflect the updated + state, without the need to explicitly subscribe to the variable. + + Reactive objects in Solara are also context-aware, meaning that they can + maintain separate values for each browser tab or user session. This enables + each user to have their own independent state, allowing them to interact + with the web application without affecting the state of other users. + + Args: + value (T): The initial value of the reactive variable. + + Returns: + Reactive[T]: A new Reactive object with the specified initial value. + + Example: + + ```python + >>> counter = widget.reactive(0) + >>> counter.value + 0 + >>> counter.set(1) + >>> counter.value + 1 + >>> counter.value += 1 + >>> counter.value + 2 + ``` + + + ## Solara example + + Here's an example that demonstrates the use of reactive variables in Solara components: + + ```widget + import nextpy.interfaces.jupyter + + counter = widget.reactive(0) + + def increment(): + counter.value += 1 + + + @widget.component + def CounterDisplay(): + widget.Info(f"Counter: {counter.value}") + + + @widget.component + def IncrementButton(): + + widget.button("Increment", on_click=increment) + + + @widget.component + def Page(): + IncrementButton() + CounterDisplay() + ``` + + In this example, we create a reactive variable counter with an initial value of 0. + We define two components: `CounterDisplay` and `IncrementButton`. `CounterDisplay` renders the current value of counter, + while `IncrementButton` increments the value of counter when clicked. + Whenever the counter value changes, `CounterDisplay` automatically updates to display the new value. + + """ + return Reactive(value) diff --git a/nextpy/interfaces/jupyter/routing.py b/nextpy/interfaces/jupyter/routing.py new file mode 100644 index 00000000..6346152a --- /dev/null +++ b/nextpy/interfaces/jupyter/routing.py @@ -0,0 +1,267 @@ +import abc +import logging +from typing import Callable, List, Optional, Tuple, Union, cast + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter import _using_solara_server + +logger = logging.getLogger("widget.router") + + +class _LocationBase(abc.ABC): + @property + def pathname(self): + pass + + @pathname.setter + # mypy does not accept this + # @abc.abstractmethod + def pathname(self): + pass + + +class _Location(_LocationBase): + def __init__(self, pathname, setter: Callable[[str], None]) -> None: + self._pathname = pathname + self.setter = setter + + @property + def pathname(self): + return self._pathname + + @pathname.setter + def pathname(self, value): + # import pdb + + # pdb.set_trace() + self._pathname = value + self.setter(self._pathname) + + +class Router: + search: Optional[str] + + def __init__(self, path: str, routes: List[widget.Route], set_path: Callable[[str], None] = None): + # see https://developer.mozilla.org/en-US/docs/Web/API/Location for anatomy/nomenclature + if "?" in path: + self.path, self.search = path.split("?", 1) + else: + self.path = path + self.search = None + del path + self.set_path = set_path + self.parts = (self.path or "").strip("/").split("/") + self.routes = routes + self.root_path = "" + if _using_solara_server(): + import nextpy.interfaces.jupyter.server.settings + + self.root_path = widget.server.settings.main.root_path or "" + # each route in this list corresponds to a part in self.parts + self.path_routes: List["widget.Route"] = [] + self.path_routes_siblings: List[List["widget.Route"]] = [] # siblings including itself + # routes = routes.copy() + route = None + for part in self.parts: + for route in routes: + if (route.path == part) or (route.path == "/" and not part): + self.path_routes.append(route) + self.path_routes_siblings.append(routes) + routes = route.children + break + if len(self.parts) == len(self.path_routes): + # e.g. '/foo/bar' -> ['foo', 'bar'] and bar has a default route + # but if '' -> [''] we should not + route = self.path_routes[-1] + if route: + default_routes = [k for k in route.children if k.path == "/"] + if self.parts and self.parts[0] and default_routes: + self.path_routes.append(default_routes[0]) + self.path_routes_siblings.append(route.children) + + assert len(self.path_routes) == len(self.path_routes_siblings) + self.possible_match = (len(self.path_routes[-1].children) == 0) if self.path_routes else False + + def push(self, path: str): + assert self.set_path is not None + self.set_path(path) + + +router_context = widget.create_context(Router("", [])) +_location_context = widget.create_context(cast(_LocationBase, _Location("", lambda x: None))) + +route_level_context = widget.create_context(0) + + +def use_route_level(): + route_level = widget.use_context(route_level_context) + return route_level + + +def use_router() -> Router: + """Returns the current router object. + + See also [Understanding Routing](/docs/understanding/routing). + + `use_router` returns the current router object. This is useful to build custom routing. + + the router object contains the following properties/methods: + + * `path` - the current pathname (e.g. `/fruit/banana`) + * `parts` - the current pathname split into parts (e.g. `['fruit', 'banana']`) + * `search` - the current search string (e.g. `color=yellow`) + * `push(path: str)` - navigate to path (e.g. `router.push('/fruit/banana')`) + + ## Typical usage: + + ```python + import nextpy.interfaces.jupyter + + + @widget.component + def Page(): + router = widget.use_router() + + def redirect(): + router.push(f"/api/use_route") + + widget.button("Navigate using an event", on_click=redirect) + ``` + + """ + return widget.use_context(router_context) + + +def use_route( + level=0, + peek=False, +) -> Tuple[Optional[widget.Route], List[widget.Route]]: + """Returns (if found) the current route that matches the pathname, or None + + See also [Understanding Routing](/docs/understanding/routing). + + `use_route` returns (if found) the current route that matches the pathname, or None. It also returns all resolved routes of that level + (i.e. all siblings and itself). This return tuple is useful to build custom navigation (e.g. using tabs or buttons). + + + Routing starts with declaring a set of `routes` in your app (widget picks up the `routes` variable if it exists, + and it should be in the same namespace as `Page`). + In the demo below, we declared the following routes. + + ```python + routes = [ + widget.Route(path="/"), + widget.Route( + path="fruit", + component=Fruit, + children=[ + widget.Route(path="/"), + widget.Route(path="kiwi"), + widget.Route(path="banana"), + widget.Route(path="apple"), + ], + ), + ] + ``` + + Note that all routes are relative, since a component does not know if it is embedded into a larger application, which may also do routing. + Therefore you should never use the `route.path` for navigation since the route object has no knowledge of the full url + (e.g. `/api/use_route/fruit/banana`) but only knows its small piece of the pathname (e.g. `banana`) + + Use [`resolve_path`](/api/resolve_path) to request the full url for navigation, or simply use the `Link` component that can do this for us. + + If the current route has children, any child component that calls `use_route` will return the matched route and its siblings of our children. + + + ## Arguments + + * `level`: the level of the route to return. 0 is the current route, -1 is the parent route, 1 the child route, etc. + * `peek`: if True, the route level is not incremented. This is useful to peek at the next route level without changing the current route level. + """ + + router = widget.use_context(router_context) + route_level = widget.use_context(route_level_context) + if not peek: + route_level_context.provide(route_level + 1) + route_level += level + if route_level < len(router.path_routes): + return router.path_routes[route_level], router.path_routes_siblings[route_level] + else: + return None, [] + + +def find_route(path: str) -> Optional[widget.Route]: + router = widget.get_context(router_context) + route_level = min(widget.get_context(route_level_context), len(router.path_routes_siblings) - 1) + for route in router.path_routes_siblings[route_level]: + if path.startswith(route.path) or (not path and route.path == "/"): + return route + return None + + +def use_pathname(): + location_proxy = widget.use_context(_location_context) + + def setter(value): + location_proxy.pathname = value + + return location_proxy.pathname, setter + + +def resolve_path(path_or_route: Union[str, widget.Route], level=0) -> str: + """Resolve a relative path or a route to an absolute path. + + If the path is a string and starts with a `/'`, it is returned as is. + + + ## Typical usage: + + ```python + ... + route_current, routes_current_level = widget.routes() + # route_current.path == "banana" + path = widget.resolve_path(route_current) + # path == "/fruit/banana" + path_same = widget.resolve_path("banana") + # path_same == path == "/fruit/banana" + ... + ``` + + ## Arguments + + * path_or_route: a path string or a [`widget.Route`](/api/route) object to resolve. + + ## See also + + * [Multipage](/docs/howto/multipage). + * [Understanding Routing](/docs/understanding/routing). + + + """ + router = widget.get_context(router_context) + if isinstance(path_or_route, str): + path = path_or_route + if path.startswith("/"): + return path + route_level = widget.get_context(route_level_context) + level + parts = [*router.parts[:route_level], path] + path = "/" + "/".join(parts) + if path.startswith("//"): + path = path[1:] + return path + elif isinstance(path_or_route, widget.Route): + route: widget.Route = path_or_route + path = _resolve_path("/", route, router.routes) + if path.startswith("//"): + path = path[1:] + return path + + +def _resolve_path(prefix: str, findroute: widget.Route, routes: List[widget.Route]): + for route in routes: + path = (prefix + "/" + route.path) if route.path != "/" else prefix + if findroute is route: + return path + possible_path = _resolve_path(path, findroute=findroute, routes=route.children) + if possible_path is not None: + return possible_path diff --git a/nextpy/interfaces/jupyter/server/settings.py b/nextpy/interfaces/jupyter/server/settings.py new file mode 100644 index 00000000..b8fea3cb --- /dev/null +++ b/nextpy/interfaces/jupyter/server/settings.py @@ -0,0 +1,29 @@ +import os +import re +import sys +from typing import Optional +from nextpy.interfaces.jupyter.minisettings import BaseSettings + +HOST_DEFAULT = os.environ.get("HOST", "localhost") +is_mac_os_conda = "arm64-apple-darwin" in HOST_DEFAULT +is_wsl_windows = re.match(r".*?-w1[0-9]", HOST_DEFAULT) +if is_mac_os_conda or is_wsl_windows: + HOST_DEFAULT = "localhost" + +class MainSettings(BaseSettings): + use_pdb: bool = False + mode: str = "production" + tracer: bool = False + timing: bool = False + root_path: Optional[str] = None # e.g. /myapp (without trailing slash) + base_url: str = "" # e.g. https://myapp.widget.run/myapp/ + platform: str = sys.platform + host: str = HOST_DEFAULT + experimental_performance: bool = False + + class Config: + env_prefix = "solara_" + case_sensitive = False + env_file = ".env" + +main = MainSettings() \ No newline at end of file diff --git a/nextpy/interfaces/jupyter/settings.py b/nextpy/interfaces/jupyter/settings.py new file mode 100644 index 00000000..27da0f02 --- /dev/null +++ b/nextpy/interfaces/jupyter/settings.py @@ -0,0 +1,39 @@ +import os +from typing import Optional + +from .minisettings import BaseSettings, Field +from .util import get_solara_home + +try: + import dotenv +except ImportError: + pass +else: + dotenv.load_dotenv() + + +home = get_solara_home() +if not home.exists(): + try: + home.mkdir(parents=True, exist_ok=True) + except OSError: + pass # can fail in for instance docker when $HOME is not set/writable + + +class Cache(BaseSettings): + type: str = Field("memory", env="SOLARA_CACHE", title="Type of cache, e.g. 'memory', 'disk', 'redis', or a multilevel cache, e.g. 'memory,disk'") + disk_max_size: str = Field("10GB", title="Maximum size for'disk' cache , e.g. 10GB, 500MB") + memory_max_size: str = Field("1GB", title="Maximum size for 'memory-size' cache, e.g. 10GB, 500MB") + memory_max_items: int = Field(128, title="Maximum number of items for 'memory' cache") + clear: bool = Field(False, title="Clear the cache on startup, only applies to disk and redis caches") + path: Optional[str] = Field( + os.path.join(home, "cache"), env="SOLARA_CACHE_PATH", title="Storage location for 'disk' cache. Defaults to `${SOLARA_HOME}/cache`" + ) + + class Config: + env_prefix = "solara_cache_" + case_sensitive = False + env_file = ".env" + + +cache: Cache = Cache() diff --git a/nextpy/interfaces/jupyter/state.py b/nextpy/interfaces/jupyter/state.py new file mode 100644 index 00000000..721cd85c --- /dev/null +++ b/nextpy/interfaces/jupyter/state.py @@ -0,0 +1,10 @@ +import nextpy.interfaces.jupyter as xtj + +class State: + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + + # Automatically convert class attributes to state variables + for attr_name, initial_value in cls.__dict__.items(): + if not callable(initial_value): + setattr(cls, attr_name, xtj.reactive(initial_value)) diff --git a/nextpy/interfaces/jupyter/toestand.py b/nextpy/interfaces/jupyter/toestand.py new file mode 100644 index 00000000..a2065dd7 --- /dev/null +++ b/nextpy/interfaces/jupyter/toestand.py @@ -0,0 +1,582 @@ +import contextlib +import dataclasses +import logging +import sys +import threading +from abc import ABC, abstractmethod +from collections import defaultdict +from operator import getitem +from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generic, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + cast, +) + +import react_ipywidgets as react +import reacton.core +from reacton.utils import equals + +import nextpy.interfaces.jupyter as widget +from nextpy.interfaces.jupyter import _using_solara_server + +T = TypeVar("T") +TS = TypeVar("TS") +S = TypeVar("S") # used for state +logger = logging.getLogger("widget.toestand") + +_DEBUG = False + + +class ThreadLocal(threading.local): + reactive_used: Optional[Set["ValueBase"]] = None + + +thread_local = ThreadLocal() + + +# these hooks should go into react-ipywidgets +def use_sync_external_store(subscribe: Callable[[Callable[[], None]], Callable[[], None]], get_snapshot: Callable[[], Any]): + _, set_counter = react.use_state(0) + + def force_update(): + set_counter(lambda x: x + 1) + + state = get_snapshot() + prev_state = react.use_ref(state) + + def update_state(): + prev_state.current = state + + react.use_effect(update_state) + + def on_store_change(_ignore_new_state=None): + new_state = get_snapshot() + if not equals(new_state, prev_state.current): + prev_state.current = new_state + force_update() + + react.use_effect(lambda: subscribe(on_store_change), []) + return state + + +def use_sync_external_store_with_selector(subscribe, get_snapshot: Callable[[], Any], selector): + return use_sync_external_store(subscribe, lambda: selector(get_snapshot())) + + +def merge_state(d1: S, **kwargs) -> S: + if dataclasses.is_dataclass(d1): + return dataclasses.replace(d1, **kwargs) + if "pydantic" in sys.modules and isinstance(d1, sys.modules["pydantic"].BaseModel): + return type(d1)(**{**d1.dict(), **kwargs}) # type: ignore + return cast(S, {**cast(dict, d1), **kwargs}) + + +class ValueBase(Generic[T]): + def __init__(self, merge: Callable = merge_state): + self.merge = merge + self.listeners: Dict[str, Set[Tuple[Callable[[T], None], Optional[ContextManager]]]] = defaultdict(set) + self.listeners2: Dict[str, Set[Tuple[Callable[[T, T], None], Optional[ContextManager]]]] = defaultdict(set) + + @property + def lock(self): + raise NotImplementedError + + @property + def value(self) -> T: + return self.get() + + @value.setter + def value(self, value: T): + self.set(value) + + def set(self, value: T): + raise NotImplementedError + + def get(self) -> T: + raise NotImplementedError + + def _get_scope_key(self): + raise NotImplementedError + + def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None): + scope_id = self._get_scope_key() + self.listeners[scope_id].add((listener, scope)) + + def cleanup(): + self.listeners[scope_id].remove((listener, scope)) + + return cleanup + + def subscribe_change(self, listener: Callable[[T, T], None], scope: Optional[ContextManager] = None): + scope_id = self._get_scope_key() + self.listeners2[scope_id].add((listener, scope)) + + def cleanup(): + self.listeners2[scope_id].remove((listener, scope)) + + return cleanup + + def fire(self, new: T, old: T): + logger.info("value change from %s to %s, will fire events", old, new) + scope_id = self._get_scope_key() + scopes = set() + for listener, scope in self.listeners[scope_id].copy(): + if scope is not None: + scopes.add(scope) + for listener2, scope in self.listeners2[scope_id].copy(): + if scope is not None: + scopes.add(scope) + stack = contextlib.ExitStack() + with contextlib.ExitStack() as stack: + for scope in scopes: + stack.enter_context(scope) + for listener, scope in self.listeners[scope_id].copy(): + listener(new) + for listener2, scope in self.listeners2[scope_id].copy(): + listener2(new, old) + + def update(self, _f=None, **kwargs): + if _f is not None: + assert not kwargs + with self.lock: + kwargs = _f(self.get()) + with self.lock: + # important to have this part thread-safe + new = self.merge(self.get(), **kwargs) + self.set(new) + + def use_value(self) -> T: + # .use with the default argument doesn't give good type inference + return self.use() + + def use(self, selector: Callable[[T], TS] = lambda x: x) -> TS: # type: ignore + return selector(self.value) + + def use_state(self) -> Tuple[T, Callable[[T], None]]: + setter = self.set + value = self.use() # type: ignore + return value, setter + + @property + def fields(self) -> T: + # we lie about the return type, but in combination with + # setter we can make type safe setters (see docs/tests) + return cast(T, Fields(self)) + + def setter(self, field: TS) -> Callable[[TS], None]: + _field = cast(FieldBase, field) + + def setter(new_value: TS): + _field.set(new_value) + + return cast(Callable[[TS], None], setter) + + +# the default store for now, stores in a global dict, or when in a widget +# context, in the widget user context + + +class KernelStore(ValueBase[S], ABC): + _global_dict: Dict[str, S] = {} # outside of widget context, this is used + # we keep a counter per type, so the storage keys we generate are deterministic + _type_counter: Dict[Type, int] = defaultdict(int) + scope_lock = threading.Lock() + + def __init__(self, key=None): + super().__init__() + self.storage_key = key + self._global_dict = {} + # since a set can trigger events, which can trigger new updates, we need a recursive lock + self._lock = threading.RLock() + self.local = threading.local() + + @property + def lock(self): + return self._lock + + def _get_scope_key(self): + scope_dict, scope_id = self._get_dict() + return scope_id + + def _get_dict(self): + scope_dict = self._global_dict + scope_id = "global" + if _using_solara_server(): + import nextpy.interfaces.jupyter.server.kernel_context + + try: + context = widget.server.kernel_context.get_current_context() + except RuntimeError: # noqa + pass # do we need to be more strict? + else: + scope_dict = cast(Dict[str, S], context.user_dicts) + scope_id = context.id + return cast(Dict[str, S], scope_dict), scope_id + + def get(self): + scope_dict, scope_id = self._get_dict() + if self.storage_key not in scope_dict: + with self.scope_lock: + if self.storage_key not in scope_dict: + # we assume immutable, so don't make a copy + scope_dict[self.storage_key] = self.initial_value() + return scope_dict[self.storage_key] + + def set(self, value: S): + scope_dict, scope_id = self._get_dict() + old = self.get() + if equals(old, value): + return + scope_dict[self.storage_key] = value + + if _DEBUG: + import traceback + + traceback.print_stack(limit=17, file=sys.stdout) + + print("change old", old) # noqa + print("change new", value) # noqa + + self.fire(value, old) + + @abstractmethod + def initial_value(self) -> S: + pass + + +class KernelStoreValue(KernelStore[S]): + default_value: S + + def __init__(self, default_value: S, key=None): + self.default_value = default_value + cls = type(default_value) + if key is None: + with KernelStoreValue.scope_lock: + index = self._type_counter[cls] + self._type_counter[cls] += 1 + key = cls.__module__ + ":" + cls.__name__ + ":" + str(index) + super().__init__(key=key) + + def initial_value(self) -> S: + return self.default_value + + +class Reactive(ValueBase[S]): + _storage: ValueBase[S] + + def __init__(self, default_value: Union[S, ValueBase[S]], key=None): + super().__init__() + if not isinstance(default_value, ValueBase): + self._storage = KernelStoreValue(default_value, key=key) + else: + self._storage = default_value + self.__post__init__() + self._name = None + self._owner = None + + def __set_name__(self, owner, name): + self._name = name + self._owner = owner + + def __repr__(self): + value = self.get(add_watch=False) + if self._name: + return f"" + else: + return f"" + + def __str__(self): + if self._name: + return f"{self._owner.__name__}.{self._name}={self.value!r}" + else: + return f"{self.value!r}" + + @property + def lock(self): + return self._storage.lock + + def __post__init__(self): + pass + + def update(self, *args, **kwargs): + self._storage.update(*args, **kwargs) + + def set(self, value: S): + if value is self: + raise ValueError("Can't set a reactive to itself") + self._storage.set(value) + + def get(self, add_watch=True) -> S: + if add_watch and thread_local.reactive_used is not None: + thread_local.reactive_used.add(self) + return self._storage.get() + + def subscribe(self, listener: Callable[[S], None], scope: Optional[ContextManager] = None): + return self._storage.subscribe(listener, scope=scope) + + def subscribe_change(self, listener: Callable[[S, S], None], scope: Optional[ContextManager] = None): + return self._storage.subscribe_change(listener, scope=scope) + + def computed(self, f: Callable[[S], T]) -> "Computed[T]": + return Computed(f, self) + + +class Computed(Generic[T]): + def __init__(self, compute: Callable[[S], T], state: Reactive[S]): + self.compute = compute + self.state = state + + def get(self) -> T: + return self.compute(self.state.get()) + + def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None): + return self.state.subscribe(lambda _: listener(self.get()), scope=scope) + + def use(self, selector: Callable[[T], T]) -> T: + slice = use_sync_external_store_with_selector( + self.subscribe, + self.get, + selector, + ) + return slice + + +class ValueSubField(ValueBase[T]): + def __init__(self, field: "FieldBase"): + super().__init__() # type: ignore + self._field = field + field = field + while not isinstance(field, ValueBase): + field = field._parent + self._root = field + assert isinstance(self._root, ValueBase) + + def __str__(self): + return str(self._field) + + def __repr__(self): + return f"" + + @property + def lock(self): + return self._root.lock + + def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None): + def on_change(new, old): + try: + new_value = self._field.get(new) + except IndexError: + return # the current design choice to silently drop the update message + except KeyError: + return # same + old_value = self._field.get(old) + if not equals(new_value, old_value): + listener(new_value) + + return self._root.subscribe_change(on_change, scope=scope) + + def subscribe_change(self, listener: Callable[[T, T], None], scope: Optional[ContextManager] = None): + def on_change(new, old): + try: + new_value = self._field.get(new) + except IndexError: + return # see subscribe + except KeyError: + return # see subscribe + old_value = self._field.get(old) + if not equals(new_value, old_value): + listener(new_value, old_value) + + return self._root.subscribe_change(on_change, scope=scope) + + def get(self, obj=None, add_watch=True) -> T: + if add_watch and thread_local.reactive_used is not None: + thread_local.reactive_used.add(self) + return self._field.get(obj) + + def set(self, value: T): + self._field.set(value) + + +def Ref(field: T) -> Reactive[T]: + _field = cast(FieldBase, field) + return Reactive[T](ValueSubField[T](_field)) + + +class FieldBase: + _parent: Any + + def __getattr__(self, key): + if key in ["_parent", "set", "_lock"] or key.startswith("__"): + return self.__dict__[key] + return FieldAttr(self, key) + + def __getitem__(self, key): + return FieldItem(self, key) + + def get(self, obj=None): + raise NotImplementedError + + def set(self, value): + raise NotImplementedError + + +class Fields(FieldBase): + def __init__(self, state: ValueBase): + self._parent = state + self._lock = state.lock + + def get(self, obj=None): + # we are at the root, so override the object + # so we can get the 'old' value + if obj is not None: + return obj + return self._parent.get(add_watch=False) + + def set(self, value): + self._parent.set(value) + + def __repr__(self): + return repr(self._parent) + + +class FieldAttr(FieldBase): + def __init__(self, parent, key: str): + self._parent = parent + self.key = key + self._lock = parent._lock + + def get(self, obj=None): + obj = self._parent.get(obj) + return getattr(obj, self.key) + + def set(self, value): + with self._lock: + parent_value = self._parent.get() + if isinstance(self.key, str): + parent_value = merge_state(parent_value, **{self.key: value}) + self._parent.set(parent_value) + else: + raise TypeError(f"Type of key {self.key!r} is not supported") + + def __str__(self): + return f".{self.key}" + + def __repr__(self): + return f"" + + +class FieldItem(FieldBase): + def __init__(self, parent, key: str): + self._parent = parent + self.key = key + self._lock = parent._lock + + def get(self, obj=None): + obj = self._parent.get(obj) + return getitem(obj, self.key) + + def set(self, value): + with self._lock: + parent_value = self._parent.get() + if isinstance(self.key, int) and isinstance(parent_value, (list, tuple)): + parent_type = type(parent_value) + parent_value = parent_value.copy() # type: ignore + parent_value[self.key] = value + self._parent.set(parent_type(parent_value)) + else: + parent_value = merge_state(parent_value, **{self.key: value}) + self._parent.set(parent_value) + + +class AutoSubscribeContextManagerBase: + # a render loop might trigger a new render loop of a differtent render context + # so we want to save, and restore the current reactive_used + reactive_used: Optional[Set[ValueBase]] = None + reactive_added_previous_run: Optional[Set[ValueBase]] = None + subscribed: Dict[ValueBase, Callable] + + def __init__(self): + self.subscribed = {} + + def update_subscribers(self, change_handler, scope=None): + assert self.reactive_used is not None + reactive_used = self.reactive_used + # remove subfields for which we already listen to it's root reactive value + reactive_used_subfields = {k for k in reactive_used if isinstance(k, ValueSubField)} + reactive_used = reactive_used - reactive_used_subfields + # only add subfield for which we don't listen to it's parent + for reactive_used_subfield in reactive_used_subfields: + if reactive_used_subfield._root not in reactive_used: + reactive_used.add(reactive_used_subfield) + added = reactive_used - (self.reactive_added_previous_run or set()) + + removed = (self.reactive_added_previous_run or set()) - reactive_used + + for reactive in added: + if reactive not in self.subscribed: + unsubscribe = reactive.subscribe_change(change_handler, scope=scope) + self.subscribed[reactive] = unsubscribe + for reactive in removed: + unsubscribe = self.subscribed[reactive] + unsubscribe() + del self.subscribed[reactive] + self.reactive_added_previous_run = added + + def unsubscribe_all(self): + for reactive in self.subscribed: + unsubscribe = self.subscribed[reactive] + unsubscribe() + + def __enter__(self): + self.reactive_used_before = thread_local.reactive_used + self.reactive_used = thread_local.reactive_used = set() + assert thread_local.reactive_used is self.reactive_used, f"{hex(id(thread_local.reactive_used))} vs {hex(id(self.reactive_used))}" + + def __exit__(self, exc_type, exc_val, exc_tb): + thread_local.reactive_used = self.reactive_used_before + + +class AutoSubscribeContextManagerReacton(AutoSubscribeContextManagerBase): + def __init__(self, element: widget.Element): + self.element = element + super().__init__() + + def __enter__(self): + _, set_counter = widget.use_state(0, key="auto_subscribe_force_update_counter") + + def force_update(new_value, old_value): + # can we do just x+1 to collapse multiple updates into one? + set_counter(lambda x: x + 1) + + super().__enter__() + + def update_subscribers(): + self.update_subscribers(force_update, scope=reacton.core.get_render_context(required=True)) + + widget.use_effect(update_subscribers, None) + + def on_close(): + def cleanup(): + assert self.reactive_added_previous_run is not None + self.unsubscribe_all() + + return cleanup + + widget.use_effect(on_close, []) + + +# alias for compatibility +State = Reactive + +auto_subscribe_context_manager = AutoSubscribeContextManagerReacton +reacton.core._component_context_manager_classes.append(auto_subscribe_context_manager) diff --git a/nextpy/interfaces/jupyter/util.py b/nextpy/interfaces/jupyter/util.py new file mode 100644 index 00000000..5252efdf --- /dev/null +++ b/nextpy/interfaces/jupyter/util.py @@ -0,0 +1,271 @@ +import base64 +import contextlib +import hashlib +import os +import sys +import threading +from collections import abc +from pathlib import Path +from typing import TYPE_CHECKING, Dict, List, Tuple, Union + +if TYPE_CHECKING: + import numpy as np + +import ipyvuetify +import ipywidgets +import reacton + +import nextpy.interfaces.jupyter as widget + +SOLARA_ALLOW_OTHER_TRACER = os.environ.get("SOLARA_ALLOW_OTHER_TRACER", False) in (True, "True", "true", "1") +ipyvuetify_major_version = int(ipyvuetify.__version__.split(".")[0]) +ipywidgets_major = int(ipywidgets.__version__.split(".")[0]) + + +def github_url(file): + rel_path = os.path.relpath(file, Path(widget.__file__).parent.parent) + github_url = widget.github_url + f"/blob/{widget.git_branch}/" + rel_path + return github_url + + +def github_edit_url(file): + # e.g. https://github.com/widgetti/widget/edit/master/widget/__init__.py + rel_path = os.path.relpath(file, Path(widget.__file__).parent.parent) + github_url = widget.github_url + f"/edit/{widget.git_branch}/" + rel_path + return github_url + + +def load_file_as_data_url(file_name, mime): + with open(file_name, "rb") as f: + data = f.read() + return f"data:{mime};base64," + base64.b64encode(data).decode("utf-8") + + +def isinstanceof(object, spec: str): + """Check if object is instance of type ':' + + This can avoid a runtime dependency, since we do not need to import `modulename`. + + >>> import numpy as np + >>> isinstanceof(np.arange(2), "numpy:ndarray") + True + """ + module_name, classname = spec.split(":") + module = sys.modules.get(module_name) + if module: + cls = getattr(module, classname) + return isinstance(object, cls) + return False + + +def numpy_to_image(data: "np.ndarray", format="png"): + import io + + if data.ndim == 3: + try: + import PIL.Image + except ModuleNotFoundError: + raise ModuleNotFoundError("Pillow is required to convert numpy array to image, use pip install pillow to install it.") + if data.shape[2] == 3: + im = PIL.Image.fromarray(data[::], "RGB") + elif data.shape[2] == 4: + im = PIL.Image.fromarray(data[::], "RGBA") + else: + raise ValueError(f"Expected last dimension to have 3 or 4 dimensions, total shape we got was {data.shape}") + f = io.BytesIO() + im.save(f, format) + return f.getvalue() + else: + raise ValueError(f"Expected an image with 3 dimensions (height, width, channel), not {data.shape}") + + +@contextlib.contextmanager +def cwd(path): + cwd = os.getcwd() + try: + os.chdir(path) + yield + finally: + os.chdir(cwd) + + +def numpy_equals(a, b): + import numpy as np + + if a is b: + return True + if a is None or b is None: + return False + if np.all(a == b): + return True + return False + + +def _combine_classes(class_list: List[str]) -> str: + return " ".join(class_list) + + +def _flatten_style(style: Union[str, Dict, None] = None) -> str: + if style is None: + return "" + elif isinstance(style, str): + return style + elif isinstance(style, dict): + return ";".join(f"{k}:{v}" for k, v in style.items()) + ";" + else: + raise ValueError(f"Expected style to be a string or dict, got {type(style)}") + + +def import_item(name: str): + """Import an object by name like widget.cache.LRU""" + parts = name.rsplit(".", 2) + if len(parts) == 1: + return __import__(name) + else: + # TODO - fix later + import nextpy.interfaces.jupyter as widget + module = __import__(".".join(parts[:-1]), fromlist=[parts[-1]]) + return getattr(module, parts[-1]) + + +def get_solara_home() -> Path: + """Get widget home directory, defaults to ~/.widget. + + The $SOLARA_HOME environment variable can be set to override this default. + + If $SOLARA_HOME is not defined and ~ cannot be expanded, the current working directory + ".widget" is used. + """ + os_home = None + try: + os_home = Path.home() + except Exception: + pass + if "SOLARA_HOME" in os.environ: + return Path(os.environ["SOLARA_HOME"]) + elif os_home: + return os_home / ".widget" + else: + return Path(os.getcwd()) / ".widget" + + +def parse_size(size: str) -> int: + """Given a human readable size, return the number of bytes. + + Supports GB, MB, KB, and bytes. E.g. 10GB, 500MB, 1KB, 1000 + + Commas and _ are ignored, e.g. 1,000,000 is the same as 1000000. + """ + size = size.replace(",", "").replace("_", "").upper() + if size.endswith("GB"): + return int(float(size[:-2]) * 1024 * 1024 * 1024) + elif size.endswith("MB"): + return int(float(size[:-2]) * 1024 * 1024) + elif size.endswith("KB"): + return int(float(size[:-2]) * 1024) + elif size.endswith("B"): + return int(float(size[:-1])) + else: + return int(size) + + +def nested_get(object, dotted_name: str, default=None): + names = dotted_name.split(".") + for name in names: + if isinstance(object, abc.Mapping): + if name == names[-1]: + object = object.get(name, default) + else: + object = object.get(name) + else: + if name == names[-1]: + object = getattr(object, name, default) + else: + object = getattr(object, name) + return object + + +# inherit from BaseException so less change of being caught +# in an except +class CancelledError(BaseException): + pass + + +# not available in python 3.6 +class nullcontext(contextlib.AbstractContextManager): + def __init__(self, enter_result=None): + self.enter_result = enter_result + + def __enter__(self): + return self.enter_result + + def __exit__(self, *excinfo): + pass + + +@contextlib.contextmanager +def cancel_guard(cancelled: threading.Event): + def tracefunc(frame, event, arg): + # this gets called at least for every line executed + if cancelled.is_set(): + rc = reacton.core.get_render_context(required=False) + # we do not want to cancel the rendering cycle + if rc is None or not rc._is_rendering: + # this will bubble up + raise CancelledError() + if prev and SOLARA_ALLOW_OTHER_TRACER: + prev(frame, event, arg) + # keep tracing: + return tracefunc + + # see https://docs.python.org/3/library/sys.html#sys.settrace + # it is for the calling thread only + # not every Python implementation has it + prev = None + if hasattr(sys, "gettrace"): + prev = sys.gettrace() + if hasattr(sys, "settrace"): + sys.settrace(tracefunc) + try: + yield + finally: + if hasattr(sys, "settrace"): + sys.settrace(prev) + + +def parse_timedelta(size: str) -> float: + """Turn a human readable time delta into seconds. + Supports days(d), hours (h), minutes (m) and seconds (s). + If not unit is specified, seconds is assumed. + >>> parse_timedelta("1d") + 86400 + >>> parse_timedelta("1h") + 3600 + >>> parse_timedelta("30m") + 1800 + >>> parse_timedelta("10s") + 10 + >>> parse_timedelta("10") + 10 + """ + if size.endswith("d"): + return float(size[:-1]) * 24 * 60 * 60 + elif size.endswith("h"): + return float(size[:-1]) * 60 * 60 + elif size.endswith("m"): + return float(size[:-1]) * 60 + elif size.endswith("s"): + return float(size[:-1]) + else: + return float(size) + + +def get_file_hash(path: Path, algorithm="md5") -> Tuple[bytes, str]: + """Compute the hash of a file. Note that we also return the file content as bytes.""" + data = path.read_bytes() + if sys.version_info[:2] < (3, 9): + # usedforsecurity is only available in Python 3.9+ + h = hashlib.new(algorithm) + else: + h = hashlib.new(algorithm, usedforsecurity=False) # type: ignore + h.update(data) + return data, h.hexdigest() diff --git a/nextpy/interfaces/jupyter/widgets/__init__.py b/nextpy/interfaces/jupyter/widgets/__init__.py new file mode 100644 index 00000000..99a53955 --- /dev/null +++ b/nextpy/interfaces/jupyter/widgets/__init__.py @@ -0,0 +1 @@ +from .widgets import * # noqa: F401, F403 diff --git a/nextpy/interfaces/jupyter/widgets/vue/gridlayout.vue b/nextpy/interfaces/jupyter/widgets/vue/gridlayout.vue new file mode 100644 index 00000000..6ccdb631 --- /dev/null +++ b/nextpy/interfaces/jupyter/widgets/vue/gridlayout.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/nextpy/interfaces/jupyter/widgets/vue/html.vue b/nextpy/interfaces/jupyter/widgets/vue/html.vue new file mode 100644 index 00000000..261e26eb --- /dev/null +++ b/nextpy/interfaces/jupyter/widgets/vue/html.vue @@ -0,0 +1,18 @@ + + + diff --git a/nextpy/interfaces/jupyter/widgets/vue/navigator.vue b/nextpy/interfaces/jupyter/widgets/vue/navigator.vue new file mode 100644 index 00000000..f7e81cf1 --- /dev/null +++ b/nextpy/interfaces/jupyter/widgets/vue/navigator.vue @@ -0,0 +1,98 @@ + +​ + diff --git a/nextpy/interfaces/jupyter/widgets/vue/vegalite.vue b/nextpy/interfaces/jupyter/widgets/vue/vegalite.vue new file mode 100644 index 00000000..ad9f09a7 --- /dev/null +++ b/nextpy/interfaces/jupyter/widgets/vue/vegalite.vue @@ -0,0 +1,115 @@ + + diff --git a/nextpy/interfaces/jupyter/widgets/widgets.py b/nextpy/interfaces/jupyter/widgets/widgets.py new file mode 100644 index 00000000..9e5fcc7b --- /dev/null +++ b/nextpy/interfaces/jupyter/widgets/widgets.py @@ -0,0 +1,57 @@ +import os + +import ipyvuetify as v +import ipywidgets +import traitlets + +__all__ = [ + "VegaLite", + "Navigator", + "GridLayout", + "html", + "watch", +] + + +class VegaLite(v.VuetifyTemplate): + template_file = os.path.realpath(os.path.join(os.path.dirname(__file__), "vue/vegalite.vue")) + spec = traitlets.Dict().tag(sync=True) + listen_to_click = traitlets.Bool(False).tag(sync=True) + listen_to_hover = traitlets.Bool(False).tag(sync=True) + on_click = traitlets.traitlets.Callable(None, allow_none=True) + on_hover = traitlets.traitlets.Callable(None, allow_none=True) + + def vue_altair_click(self, *args): + if self.on_click: + self.on_click(*args) + + def vue_altair_hover(self, *args): + if self.on_hover: + self.on_hover(*args) + + +class Navigator(v.VuetifyTemplate): + template_file = os.path.realpath(os.path.join(os.path.dirname(__file__), "vue/navigator.vue")) + location = traitlets.Unicode(None, allow_none=True).tag(sync=True) + + +class GridLayout(v.VuetifyTemplate): + template_file = os.path.join(os.path.dirname(__file__), "vue/gridlayout.vue") + gridlayout_loaded = traitlets.Bool(False).tag(sync=True) + items = traitlets.Union([traitlets.List(), traitlets.Dict()], default_value=[]).tag(sync=True, **ipywidgets.widget_serialization) + grid_layout = traitlets.List(default_value=[]).tag(sync=True) + draggable = traitlets.CBool(True).tag(sync=True) + resizable = traitlets.CBool(True).tag(sync=True) + + +class html(v.VuetifyTemplate): + template_file = os.path.realpath(os.path.join(os.path.dirname(__file__), "vue/html.vue")) + tag = traitlets.Unicode("div").tag(sync=True) + attributes = traitlets.Dict().tag(sync=True) + unsafe_innerHTML = traitlets.Unicode(None, allow_none=True).tag(sync=True) + + +def watch(): + import ipyvue + + ipyvue.watch(os.path.realpath(os.path.dirname(__file__) + "/vue")) diff --git a/nextpy/frontend/templates/apps/base/README.md b/nextpy/interfaces/templates/apps/base/README.md similarity index 100% rename from nextpy/frontend/templates/apps/base/README.md rename to nextpy/interfaces/templates/apps/base/README.md diff --git a/nextpy/frontend/templates/apps/base/assets/favicon.ico b/nextpy/interfaces/templates/apps/base/assets/favicon.ico similarity index 100% rename from nextpy/frontend/templates/apps/base/assets/favicon.ico rename to nextpy/interfaces/templates/apps/base/assets/favicon.ico diff --git a/nextpy/frontend/templates/apps/base/assets/github.svg b/nextpy/interfaces/templates/apps/base/assets/github.svg similarity index 100% rename from nextpy/frontend/templates/apps/base/assets/github.svg rename to nextpy/interfaces/templates/apps/base/assets/github.svg diff --git a/nextpy/frontend/templates/apps/base/assets/gradient_underline.svg b/nextpy/interfaces/templates/apps/base/assets/gradient_underline.svg similarity index 100% rename from nextpy/frontend/templates/apps/base/assets/gradient_underline.svg rename to nextpy/interfaces/templates/apps/base/assets/gradient_underline.svg diff --git a/nextpy/frontend/templates/apps/base/assets/icon.svg b/nextpy/interfaces/templates/apps/base/assets/icon.svg similarity index 100% rename from nextpy/frontend/templates/apps/base/assets/icon.svg rename to nextpy/interfaces/templates/apps/base/assets/icon.svg diff --git a/nextpy/frontend/templates/apps/base/assets/logo_darkmode.svg b/nextpy/interfaces/templates/apps/base/assets/logo_darkmode.svg similarity index 100% rename from nextpy/frontend/templates/apps/base/assets/logo_darkmode.svg rename to nextpy/interfaces/templates/apps/base/assets/logo_darkmode.svg diff --git a/nextpy/frontend/templates/apps/base/assets/paneleft.svg b/nextpy/interfaces/templates/apps/base/assets/paneleft.svg similarity index 100% rename from nextpy/frontend/templates/apps/base/assets/paneleft.svg rename to nextpy/interfaces/templates/apps/base/assets/paneleft.svg diff --git a/nextpy/frontend/templates/apps/base/assets/text_logo_darkmode.svg b/nextpy/interfaces/templates/apps/base/assets/text_logo_darkmode.svg similarity index 100% rename from nextpy/frontend/templates/apps/base/assets/text_logo_darkmode.svg rename to nextpy/interfaces/templates/apps/base/assets/text_logo_darkmode.svg diff --git a/nextpy/frontend/templates/apps/base/code/__init__.py b/nextpy/interfaces/templates/apps/base/code/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/__init__.py rename to nextpy/interfaces/templates/apps/base/code/__init__.py diff --git a/nextpy/frontend/templates/apps/base/code/base.py b/nextpy/interfaces/templates/apps/base/code/base.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/base.py rename to nextpy/interfaces/templates/apps/base/code/base.py diff --git a/nextpy/frontend/templates/apps/base/code/components/__init__.py b/nextpy/interfaces/templates/apps/base/code/components/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/components/__init__.py rename to nextpy/interfaces/templates/apps/base/code/components/__init__.py diff --git a/nextpy/frontend/templates/apps/base/code/components/sidebar.py b/nextpy/interfaces/templates/apps/base/code/components/sidebar.py similarity index 98% rename from nextpy/frontend/templates/apps/base/code/components/sidebar.py rename to nextpy/interfaces/templates/apps/base/code/components/sidebar.py index 8bc48915..b6b92ad0 100644 --- a/nextpy/frontend/templates/apps/base/code/components/sidebar.py +++ b/nextpy/interfaces/templates/apps/base/code/components/sidebar.py @@ -119,7 +119,7 @@ def sidebar() -> xt.Component: The sidebar component. """ # Get all the decorated pages and add them to the sidebar. - from nextpy.frontend.page import get_decorated_pages + from nextpy.interfaces.web.page import get_decorated_pages return xt.box( xt.vstack( diff --git a/nextpy/frontend/templates/apps/base/code/pages/__init__.py b/nextpy/interfaces/templates/apps/base/code/pages/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/pages/__init__.py rename to nextpy/interfaces/templates/apps/base/code/pages/__init__.py diff --git a/nextpy/frontend/templates/apps/base/code/pages/dashboard.py b/nextpy/interfaces/templates/apps/base/code/pages/dashboard.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/pages/dashboard.py rename to nextpy/interfaces/templates/apps/base/code/pages/dashboard.py diff --git a/nextpy/frontend/templates/apps/base/code/pages/index.py b/nextpy/interfaces/templates/apps/base/code/pages/index.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/pages/index.py rename to nextpy/interfaces/templates/apps/base/code/pages/index.py diff --git a/nextpy/frontend/templates/apps/base/code/pages/settings.py b/nextpy/interfaces/templates/apps/base/code/pages/settings.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/pages/settings.py rename to nextpy/interfaces/templates/apps/base/code/pages/settings.py diff --git a/nextpy/frontend/templates/apps/base/code/styles.py b/nextpy/interfaces/templates/apps/base/code/styles.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/styles.py rename to nextpy/interfaces/templates/apps/base/code/styles.py diff --git a/nextpy/frontend/templates/apps/base/code/templates/__init__.py b/nextpy/interfaces/templates/apps/base/code/templates/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/base/code/templates/__init__.py rename to nextpy/interfaces/templates/apps/base/code/templates/__init__.py diff --git a/nextpy/frontend/templates/apps/base/code/templates/template.py b/nextpy/interfaces/templates/apps/base/code/templates/template.py similarity index 98% rename from nextpy/frontend/templates/apps/base/code/templates/template.py rename to nextpy/interfaces/templates/apps/base/code/templates/template.py index d83a9e69..f09408e1 100644 --- a/nextpy/frontend/templates/apps/base/code/templates/template.py +++ b/nextpy/interfaces/templates/apps/base/code/templates/template.py @@ -26,7 +26,7 @@ def menu_button() -> xt.Component: Returns: The menu button component. """ - from nextpy.frontend.page import get_decorated_pages + from nextpy.interfaces.web.page import get_decorated_pages return xt.box( xt.menu( diff --git a/nextpy/frontend/templates/apps/blank/assets/favicon.ico b/nextpy/interfaces/templates/apps/blank/assets/favicon.ico similarity index 100% rename from nextpy/frontend/templates/apps/blank/assets/favicon.ico rename to nextpy/interfaces/templates/apps/blank/assets/favicon.ico diff --git a/nextpy/frontend/templates/apps/blank/assets/github.svg b/nextpy/interfaces/templates/apps/blank/assets/github.svg similarity index 100% rename from nextpy/frontend/templates/apps/blank/assets/github.svg rename to nextpy/interfaces/templates/apps/blank/assets/github.svg diff --git a/nextpy/frontend/templates/apps/blank/assets/gradient_underline.svg b/nextpy/interfaces/templates/apps/blank/assets/gradient_underline.svg similarity index 100% rename from nextpy/frontend/templates/apps/blank/assets/gradient_underline.svg rename to nextpy/interfaces/templates/apps/blank/assets/gradient_underline.svg diff --git a/nextpy/frontend/templates/apps/blank/assets/icon.svg b/nextpy/interfaces/templates/apps/blank/assets/icon.svg similarity index 100% rename from nextpy/frontend/templates/apps/blank/assets/icon.svg rename to nextpy/interfaces/templates/apps/blank/assets/icon.svg diff --git a/nextpy/frontend/templates/apps/blank/assets/logo_darkmode.svg b/nextpy/interfaces/templates/apps/blank/assets/logo_darkmode.svg similarity index 100% rename from nextpy/frontend/templates/apps/blank/assets/logo_darkmode.svg rename to nextpy/interfaces/templates/apps/blank/assets/logo_darkmode.svg diff --git a/nextpy/frontend/templates/apps/blank/assets/paneleft.svg b/nextpy/interfaces/templates/apps/blank/assets/paneleft.svg similarity index 100% rename from nextpy/frontend/templates/apps/blank/assets/paneleft.svg rename to nextpy/interfaces/templates/apps/blank/assets/paneleft.svg diff --git a/nextpy/frontend/templates/apps/blank/assets/text_logo_darkmode.svg b/nextpy/interfaces/templates/apps/blank/assets/text_logo_darkmode.svg similarity index 100% rename from nextpy/frontend/templates/apps/blank/assets/text_logo_darkmode.svg rename to nextpy/interfaces/templates/apps/blank/assets/text_logo_darkmode.svg diff --git a/nextpy/frontend/templates/apps/blank/code/__init__.py b/nextpy/interfaces/templates/apps/blank/code/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/blank/code/__init__.py rename to nextpy/interfaces/templates/apps/blank/code/__init__.py diff --git a/nextpy/frontend/templates/apps/blank/code/blank.py b/nextpy/interfaces/templates/apps/blank/code/blank.py similarity index 100% rename from nextpy/frontend/templates/apps/blank/code/blank.py rename to nextpy/interfaces/templates/apps/blank/code/blank.py diff --git a/nextpy/frontend/templates/apps/hello/.gitignore b/nextpy/interfaces/templates/apps/hello/.gitignore similarity index 100% rename from nextpy/frontend/templates/apps/hello/.gitignore rename to nextpy/interfaces/templates/apps/hello/.gitignore diff --git a/nextpy/frontend/templates/apps/hello/assets/favicon.ico b/nextpy/interfaces/templates/apps/hello/assets/favicon.ico similarity index 100% rename from nextpy/frontend/templates/apps/hello/assets/favicon.ico rename to nextpy/interfaces/templates/apps/hello/assets/favicon.ico diff --git a/nextpy/frontend/templates/apps/hello/assets/github.svg b/nextpy/interfaces/templates/apps/hello/assets/github.svg similarity index 100% rename from nextpy/frontend/templates/apps/hello/assets/github.svg rename to nextpy/interfaces/templates/apps/hello/assets/github.svg diff --git a/nextpy/frontend/templates/apps/hello/assets/gradient_underline.svg b/nextpy/interfaces/templates/apps/hello/assets/gradient_underline.svg similarity index 100% rename from nextpy/frontend/templates/apps/hello/assets/gradient_underline.svg rename to nextpy/interfaces/templates/apps/hello/assets/gradient_underline.svg diff --git a/nextpy/frontend/templates/apps/hello/assets/icon.svg b/nextpy/interfaces/templates/apps/hello/assets/icon.svg similarity index 100% rename from nextpy/frontend/templates/apps/hello/assets/icon.svg rename to nextpy/interfaces/templates/apps/hello/assets/icon.svg diff --git a/nextpy/frontend/templates/apps/hello/assets/logo_darkmode.svg b/nextpy/interfaces/templates/apps/hello/assets/logo_darkmode.svg similarity index 100% rename from nextpy/frontend/templates/apps/hello/assets/logo_darkmode.svg rename to nextpy/interfaces/templates/apps/hello/assets/logo_darkmode.svg diff --git a/nextpy/frontend/templates/apps/hello/assets/paneleft.svg b/nextpy/interfaces/templates/apps/hello/assets/paneleft.svg similarity index 100% rename from nextpy/frontend/templates/apps/hello/assets/paneleft.svg rename to nextpy/interfaces/templates/apps/hello/assets/paneleft.svg diff --git a/nextpy/frontend/templates/apps/hello/assets/text_logo_darkmode.svg b/nextpy/interfaces/templates/apps/hello/assets/text_logo_darkmode.svg similarity index 100% rename from nextpy/frontend/templates/apps/hello/assets/text_logo_darkmode.svg rename to nextpy/interfaces/templates/apps/hello/assets/text_logo_darkmode.svg diff --git a/nextpy/frontend/templates/apps/hello/code/__init__.py b/nextpy/interfaces/templates/apps/hello/code/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/__init__.py rename to nextpy/interfaces/templates/apps/hello/code/__init__.py diff --git a/nextpy/frontend/templates/apps/hello/code/hello.py b/nextpy/interfaces/templates/apps/hello/code/hello.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/hello.py rename to nextpy/interfaces/templates/apps/hello/code/hello.py diff --git a/nextpy/frontend/templates/apps/hello/code/pages/__init__.py b/nextpy/interfaces/templates/apps/hello/code/pages/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/pages/__init__.py rename to nextpy/interfaces/templates/apps/hello/code/pages/__init__.py diff --git a/nextpy/frontend/templates/apps/hello/code/pages/chatapp.py b/nextpy/interfaces/templates/apps/hello/code/pages/chatapp.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/pages/chatapp.py rename to nextpy/interfaces/templates/apps/hello/code/pages/chatapp.py diff --git a/nextpy/frontend/templates/apps/hello/code/pages/datatable.py b/nextpy/interfaces/templates/apps/hello/code/pages/datatable.py similarity index 99% rename from nextpy/frontend/templates/apps/hello/code/pages/datatable.py rename to nextpy/interfaces/templates/apps/hello/code/pages/datatable.py index b11b148b..6edb77dc 100644 --- a/nextpy/frontend/templates/apps/hello/code/pages/datatable.py +++ b/nextpy/interfaces/templates/apps/hello/code/pages/datatable.py @@ -5,7 +5,7 @@ from typing import Any import nextpy as xt -from nextpy.frontend.components.glide_datagrid.dataeditor import DataEditorTheme +from nextpy.interfaces.web.components.glide_datagrid.dataeditor import DataEditorTheme from ..styles import * from ..webui.state import State diff --git a/nextpy/frontend/templates/apps/hello/code/pages/forms.py b/nextpy/interfaces/templates/apps/hello/code/pages/forms.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/pages/forms.py rename to nextpy/interfaces/templates/apps/hello/code/pages/forms.py diff --git a/nextpy/frontend/templates/apps/hello/code/pages/graphing.py b/nextpy/interfaces/templates/apps/hello/code/pages/graphing.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/pages/graphing.py rename to nextpy/interfaces/templates/apps/hello/code/pages/graphing.py diff --git a/nextpy/frontend/templates/apps/hello/code/pages/home.py b/nextpy/interfaces/templates/apps/hello/code/pages/home.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/pages/home.py rename to nextpy/interfaces/templates/apps/hello/code/pages/home.py diff --git a/nextpy/frontend/templates/apps/hello/code/sidebar.py b/nextpy/interfaces/templates/apps/hello/code/sidebar.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/sidebar.py rename to nextpy/interfaces/templates/apps/hello/code/sidebar.py diff --git a/nextpy/frontend/templates/apps/hello/code/state.py b/nextpy/interfaces/templates/apps/hello/code/state.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/state.py rename to nextpy/interfaces/templates/apps/hello/code/state.py diff --git a/nextpy/frontend/templates/apps/hello/code/states/form_state.py b/nextpy/interfaces/templates/apps/hello/code/states/form_state.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/states/form_state.py rename to nextpy/interfaces/templates/apps/hello/code/states/form_state.py diff --git a/nextpy/frontend/templates/apps/hello/code/states/pie_state.py b/nextpy/interfaces/templates/apps/hello/code/states/pie_state.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/states/pie_state.py rename to nextpy/interfaces/templates/apps/hello/code/states/pie_state.py diff --git a/nextpy/frontend/templates/apps/hello/code/styles.py b/nextpy/interfaces/templates/apps/hello/code/styles.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/styles.py rename to nextpy/interfaces/templates/apps/hello/code/styles.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/__init__.py b/nextpy/interfaces/templates/apps/hello/code/webui/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/__init__.py rename to nextpy/interfaces/templates/apps/hello/code/webui/__init__.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/components/__init__.py b/nextpy/interfaces/templates/apps/hello/code/webui/components/__init__.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/components/__init__.py rename to nextpy/interfaces/templates/apps/hello/code/webui/components/__init__.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/components/chat.py b/nextpy/interfaces/templates/apps/hello/code/webui/components/chat.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/components/chat.py rename to nextpy/interfaces/templates/apps/hello/code/webui/components/chat.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/components/loading_icon.py b/nextpy/interfaces/templates/apps/hello/code/webui/components/loading_icon.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/components/loading_icon.py rename to nextpy/interfaces/templates/apps/hello/code/webui/components/loading_icon.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/components/modal.py b/nextpy/interfaces/templates/apps/hello/code/webui/components/modal.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/components/modal.py rename to nextpy/interfaces/templates/apps/hello/code/webui/components/modal.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/components/navbar.py b/nextpy/interfaces/templates/apps/hello/code/webui/components/navbar.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/components/navbar.py rename to nextpy/interfaces/templates/apps/hello/code/webui/components/navbar.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/components/sidebar.py b/nextpy/interfaces/templates/apps/hello/code/webui/components/sidebar.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/components/sidebar.py rename to nextpy/interfaces/templates/apps/hello/code/webui/components/sidebar.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/state.py b/nextpy/interfaces/templates/apps/hello/code/webui/state.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/state.py rename to nextpy/interfaces/templates/apps/hello/code/webui/state.py diff --git a/nextpy/frontend/templates/apps/hello/code/webui/styles.py b/nextpy/interfaces/templates/apps/hello/code/webui/styles.py similarity index 100% rename from nextpy/frontend/templates/apps/hello/code/webui/styles.py rename to nextpy/interfaces/templates/apps/hello/code/webui/styles.py diff --git a/nextpy/frontend/templates/jinja/app/xtconfig.py.jinja2 b/nextpy/interfaces/templates/jinja/app/xtconfig.py.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/app/xtconfig.py.jinja2 rename to nextpy/interfaces/templates/jinja/app/xtconfig.py.jinja2 diff --git a/nextpy/frontend/templates/jinja/custom_components/README.md.jinja2 b/nextpy/interfaces/templates/jinja/custom_components/README.md.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/custom_components/README.md.jinja2 rename to nextpy/interfaces/templates/jinja/custom_components/README.md.jinja2 diff --git a/nextpy/frontend/templates/jinja/custom_components/demo_app.py.jinja2 b/nextpy/interfaces/templates/jinja/custom_components/demo_app.py.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/custom_components/demo_app.py.jinja2 rename to nextpy/interfaces/templates/jinja/custom_components/demo_app.py.jinja2 diff --git a/nextpy/frontend/templates/jinja/custom_components/pyproject.toml.jinja2 b/nextpy/interfaces/templates/jinja/custom_components/pyproject.toml.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/custom_components/pyproject.toml.jinja2 rename to nextpy/interfaces/templates/jinja/custom_components/pyproject.toml.jinja2 diff --git a/nextpy/frontend/templates/jinja/custom_components/src.py.jinja2 b/nextpy/interfaces/templates/jinja/custom_components/src.py.jinja2 similarity index 96% rename from nextpy/frontend/templates/jinja/custom_components/src.py.jinja2 rename to nextpy/interfaces/templates/jinja/custom_components/src.py.jinja2 index 75c64bc5..63757146 100644 --- a/nextpy/frontend/templates/jinja/custom_components/src.py.jinja2 +++ b/nextpy/interfaces/templates/jinja/custom_components/src.py.jinja2 @@ -9,7 +9,7 @@ from typing import Any # This is because they they may not be compatible with Server-Side Rendering (SSR). # To handle this in Nextpy all you need to do is subclass NoSSRComponent instead. # For example: -# from nextpy.frontend.components.component import NoSSRComponent +# from nextpy.interfaces.web.components.component import NoSSRComponent # class {{ component_class_name }}(NoSSRComponent): # pass diff --git a/nextpy/frontend/templates/jinja/web/package.json.jinja2 b/nextpy/interfaces/templates/jinja/web/package.json.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/package.json.jinja2 rename to nextpy/interfaces/templates/jinja/web/package.json.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/_app.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/_app.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/_app.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/_app.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/_document.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/_document.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/_document.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/_document.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/base_page.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/base_page.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/base_page.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/base_page.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/component.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/component.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/component.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/component.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/custom_component.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/custom_component.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/custom_component.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/custom_component.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/index.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/index.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/index.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/index.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/stateful_component.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/stateful_component.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/stateful_component.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/stateful_component.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/stateful_components.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/stateful_components.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/stateful_components.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/stateful_components.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/pages/utils.js.jinja2 b/nextpy/interfaces/templates/jinja/web/pages/utils.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/pages/utils.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/pages/utils.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/styles/styles.css.jinja2 b/nextpy/interfaces/templates/jinja/web/styles/styles.css.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/styles/styles.css.jinja2 rename to nextpy/interfaces/templates/jinja/web/styles/styles.css.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/tailwind.config.js.jinja2 b/nextpy/interfaces/templates/jinja/web/tailwind.config.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/tailwind.config.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/tailwind.config.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/utils/context.js.jinja2 b/nextpy/interfaces/templates/jinja/web/utils/context.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/utils/context.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/utils/context.js.jinja2 diff --git a/nextpy/frontend/templates/jinja/web/utils/theme.js.jinja2 b/nextpy/interfaces/templates/jinja/web/utils/theme.js.jinja2 similarity index 100% rename from nextpy/frontend/templates/jinja/web/utils/theme.js.jinja2 rename to nextpy/interfaces/templates/jinja/web/utils/theme.js.jinja2 diff --git a/nextpy/frontend/templates/web/.gitignore b/nextpy/interfaces/templates/web/.gitignore similarity index 100% rename from nextpy/frontend/templates/web/.gitignore rename to nextpy/interfaces/templates/web/.gitignore diff --git a/nextpy/frontend/templates/web/components/nextpy/chakra_color_mode_provider.js b/nextpy/interfaces/templates/web/components/nextpy/chakra_color_mode_provider.js similarity index 100% rename from nextpy/frontend/templates/web/components/nextpy/chakra_color_mode_provider.js rename to nextpy/interfaces/templates/web/components/nextpy/chakra_color_mode_provider.js diff --git a/nextpy/frontend/templates/web/components/nextpy/radix_themes_color_mode_provider.js b/nextpy/interfaces/templates/web/components/nextpy/radix_themes_color_mode_provider.js similarity index 100% rename from nextpy/frontend/templates/web/components/nextpy/radix_themes_color_mode_provider.js rename to nextpy/interfaces/templates/web/components/nextpy/radix_themes_color_mode_provider.js diff --git a/nextpy/frontend/templates/web/jsconfig.json b/nextpy/interfaces/templates/web/jsconfig.json similarity index 100% rename from nextpy/frontend/templates/web/jsconfig.json rename to nextpy/interfaces/templates/web/jsconfig.json diff --git a/nextpy/frontend/templates/web/next.config.js b/nextpy/interfaces/templates/web/next.config.js similarity index 100% rename from nextpy/frontend/templates/web/next.config.js rename to nextpy/interfaces/templates/web/next.config.js diff --git a/nextpy/frontend/templates/web/postcss.config.js b/nextpy/interfaces/templates/web/postcss.config.js similarity index 100% rename from nextpy/frontend/templates/web/postcss.config.js rename to nextpy/interfaces/templates/web/postcss.config.js diff --git a/nextpy/frontend/templates/web/styles/code/prism.js b/nextpy/interfaces/templates/web/styles/code/prism.js similarity index 100% rename from nextpy/frontend/templates/web/styles/code/prism.js rename to nextpy/interfaces/templates/web/styles/code/prism.js diff --git a/nextpy/frontend/templates/web/styles/tailwind.css b/nextpy/interfaces/templates/web/styles/tailwind.css similarity index 100% rename from nextpy/frontend/templates/web/styles/tailwind.css rename to nextpy/interfaces/templates/web/styles/tailwind.css diff --git a/nextpy/frontend/templates/web/utils/client_side_routing.js b/nextpy/interfaces/templates/web/utils/client_side_routing.js similarity index 100% rename from nextpy/frontend/templates/web/utils/client_side_routing.js rename to nextpy/interfaces/templates/web/utils/client_side_routing.js diff --git a/nextpy/frontend/templates/web/utils/helpers/dataeditor.js b/nextpy/interfaces/templates/web/utils/helpers/dataeditor.js similarity index 100% rename from nextpy/frontend/templates/web/utils/helpers/dataeditor.js rename to nextpy/interfaces/templates/web/utils/helpers/dataeditor.js diff --git a/nextpy/frontend/templates/web/utils/helpers/range.js b/nextpy/interfaces/templates/web/utils/helpers/range.js similarity index 100% rename from nextpy/frontend/templates/web/utils/helpers/range.js rename to nextpy/interfaces/templates/web/utils/helpers/range.js diff --git a/nextpy/frontend/templates/web/utils/state.js b/nextpy/interfaces/templates/web/utils/state.js similarity index 100% rename from nextpy/frontend/templates/web/utils/state.js rename to nextpy/interfaces/templates/web/utils/state.js diff --git a/nextpy/interfaces/web/__init__.py b/nextpy/interfaces/web/__init__.py new file mode 100644 index 00000000..6fe7b8e6 --- /dev/null +++ b/nextpy/interfaces/web/__init__.py @@ -0,0 +1,16 @@ + +"""Frontend Package. + +This package forms the complete suite of the frontend code for our application, specifically crafted for client-side development tasks. Its main objectives include enhancing user interface aesthetics, optimizing user interactions, and presenting data effectively. + +Key Components: + +- `components`: Essential building blocks of Nextpy's frontend framework, these components structure the UI into modular, reusable entities, wrapping React components for efficient use. +- `imports`: A utility file designed to facilitate the import of React components into Python, streamlining the conversion of Python code to React components. +- `page`: Centered around decorators, this module enables functions to serve as distinct pages in the application, easing the process of page setup and organization. +- `style`: Dedicated to styling, this module provides dynamic and structured management of style properties, enriching the UI's visual appeal. + +Purpose and Organization: + +The Frontend Package is intricately designed to provide developers with an extensive array of tools and features for crafting a responsive, user-friendly, and high-performing frontend. Its architecture focuses on ease of use, flexibility, and scalability, making it a solid base for meeting evolving frontend requirements and creative design goals. +""" diff --git a/nextpy/frontend/components/__init__.py b/nextpy/interfaces/web/components/__init__.py similarity index 92% rename from nextpy/frontend/components/__init__.py rename to nextpy/interfaces/web/components/__init__.py index 45a21556..dc0281ec 100644 --- a/nextpy/frontend/components/__init__.py +++ b/nextpy/interfaces/web/components/__init__.py @@ -4,7 +4,7 @@ """Import all the components.""" from __future__ import annotations -from .base import Fragment, Script, fragment, script +from .base import Fragment, Script, empty, fragment, script from .chakra import * from .component import Component from .component import NoSSRComponent as NoSSRComponent @@ -18,4 +18,3 @@ from .react_player import * from .recharts import * from .suneditor import * - diff --git a/nextpy/frontend/components/base/__init__.py b/nextpy/interfaces/web/components/base/__init__.py similarity index 95% rename from nextpy/frontend/components/base/__init__.py rename to nextpy/interfaces/web/components/base/__init__.py index 3eee6704..06489383 100644 --- a/nextpy/frontend/components/base/__init__.py +++ b/nextpy/interfaces/web/components/base/__init__.py @@ -11,5 +11,6 @@ from .meta import Description, Image, Meta, Title from .script import Script +empty = Fragment.create fragment = Fragment.create script = Script.create diff --git a/nextpy/frontend/components/base/app_wrap.py b/nextpy/interfaces/web/components/base/app_wrap.py similarity index 85% rename from nextpy/frontend/components/base/app_wrap.py rename to nextpy/interfaces/web/components/base/app_wrap.py index 29842689..05c7f60c 100644 --- a/nextpy/frontend/components/base/app_wrap.py +++ b/nextpy/interfaces/web/components/base/app_wrap.py @@ -3,8 +3,8 @@ """Top-level component that wraps the entire app.""" from nextpy.backend.vars import Var -from nextpy.frontend.components.base.fragment import Fragment -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.components.base.fragment import Fragment +from nextpy.interfaces.web.components.component import Component class AppWrap(Fragment): diff --git a/nextpy/frontend/components/base/app_wrap.pyi b/nextpy/interfaces/web/components/base/app_wrap.pyi similarity index 94% rename from nextpy/frontend/components/base/app_wrap.pyi rename to nextpy/interfaces/web/components/base/app_wrap.pyi index 51c4a89b..f468c37f 100644 --- a/nextpy/frontend/components/base/app_wrap.pyi +++ b/nextpy/interfaces/web/components/base/app_wrap.pyi @@ -9,9 +9,9 @@ from typing import Any, Dict, Literal, Optional, Union, overload from nextpy.backend.vars import Var, BaseVar, ComputedVar from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style -from nextpy.frontend.components.base.fragment import Fragment -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.style import Style +from nextpy.interfaces.web.components.base.fragment import Fragment +from nextpy.interfaces.web.components.component import Component from nextpy.backend.vars import Var class AppWrap(Fragment): diff --git a/nextpy/frontend/components/base/bare.py b/nextpy/interfaces/web/components/base/bare.py similarity index 87% rename from nextpy/frontend/components/base/bare.py rename to nextpy/interfaces/web/components/base/bare.py index ce71b046..078dafb1 100644 --- a/nextpy/frontend/components/base/bare.py +++ b/nextpy/interfaces/web/components/base/bare.py @@ -7,9 +7,9 @@ from typing import Any, Iterator from nextpy.backend.vars import Var -from nextpy.frontend.components.component import Component -from nextpy.frontend.components.tags import Tag -from nextpy.frontend.components.tags.tagless import Tagless +from nextpy.interfaces.web.components.component import Component +from nextpy.interfaces.web.components.tags import Tag +from nextpy.interfaces.web.components.tags.tagless import Tagless class Bare(Component): diff --git a/nextpy/frontend/components/base/body.py b/nextpy/interfaces/web/components/base/body.py similarity index 85% rename from nextpy/frontend/components/base/body.py rename to nextpy/interfaces/web/components/base/body.py index cc4d9e81..cce7a262 100644 --- a/nextpy/frontend/components/base/body.py +++ b/nextpy/interfaces/web/components/base/body.py @@ -3,7 +3,7 @@ """Display the page body.""" -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.components.component import Component class Body(Component): diff --git a/nextpy/frontend/components/base/body.pyi b/nextpy/interfaces/web/components/base/body.pyi similarity index 96% rename from nextpy/frontend/components/base/body.pyi rename to nextpy/interfaces/web/components/base/body.pyi index 9c5f48b3..9a872398 100644 --- a/nextpy/frontend/components/base/body.pyi +++ b/nextpy/interfaces/web/components/base/body.pyi @@ -9,8 +9,8 @@ from typing import Any, Dict, Literal, Optional, Union, overload from nextpy.backend.vars import Var, BaseVar, ComputedVar from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.style import Style +from nextpy.interfaces.web.components.component import Component class Body(Component): @overload diff --git a/nextpy/frontend/components/base/document.py b/nextpy/interfaces/web/components/base/document.py similarity index 92% rename from nextpy/frontend/components/base/document.py rename to nextpy/interfaces/web/components/base/document.py index 9fc052dc..effc55c1 100644 --- a/nextpy/frontend/components/base/document.py +++ b/nextpy/interfaces/web/components/base/document.py @@ -3,7 +3,7 @@ """Document components.""" -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.components.component import Component class NextDocumentLib(Component): diff --git a/nextpy/frontend/components/base/document.pyi b/nextpy/interfaces/web/components/base/document.pyi similarity index 99% rename from nextpy/frontend/components/base/document.pyi rename to nextpy/interfaces/web/components/base/document.pyi index 7df518e0..501b8788 100644 --- a/nextpy/frontend/components/base/document.pyi +++ b/nextpy/interfaces/web/components/base/document.pyi @@ -9,8 +9,8 @@ from typing import Any, Dict, Literal, Optional, Union, overload from nextpy.backend.vars import Var, BaseVar, ComputedVar from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.style import Style +from nextpy.interfaces.web.components.component import Component class NextDocumentLib(Component): @overload diff --git a/nextpy/frontend/components/base/fragment.py b/nextpy/interfaces/web/components/base/fragment.py similarity index 89% rename from nextpy/frontend/components/base/fragment.py rename to nextpy/interfaces/web/components/base/fragment.py index 6f772265..f0fc67fd 100644 --- a/nextpy/frontend/components/base/fragment.py +++ b/nextpy/interfaces/web/components/base/fragment.py @@ -2,7 +2,7 @@ # We have rigorously tested these modifications to ensure reliability and performance. Based on successful test results, we are confident in the quality and stability of these changes. """React fragments to enable bare returns of component trees from functions.""" -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.components.component import Component class Fragment(Component): diff --git a/nextpy/frontend/components/base/fragment.pyi b/nextpy/interfaces/web/components/base/fragment.pyi similarity index 96% rename from nextpy/frontend/components/base/fragment.pyi rename to nextpy/interfaces/web/components/base/fragment.pyi index c5e967e9..976e9e1b 100644 --- a/nextpy/frontend/components/base/fragment.pyi +++ b/nextpy/interfaces/web/components/base/fragment.pyi @@ -9,8 +9,8 @@ from typing import Any, Dict, Literal, Optional, Union, overload from nextpy.backend.vars import Var, BaseVar, ComputedVar from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.style import Style +from nextpy.interfaces.web.components.component import Component class Fragment(Component): @overload diff --git a/nextpy/frontend/components/base/head.py b/nextpy/interfaces/web/components/base/head.py similarity index 86% rename from nextpy/frontend/components/base/head.py rename to nextpy/interfaces/web/components/base/head.py index b0649f5d..f3a3dabe 100644 --- a/nextpy/frontend/components/base/head.py +++ b/nextpy/interfaces/web/components/base/head.py @@ -3,7 +3,7 @@ """The head component.""" -from nextpy.frontend.components.component import Component, MemoizationLeaf +from nextpy.interfaces.web.components.component import Component, MemoizationLeaf class NextHeadLib(Component): diff --git a/nextpy/frontend/components/base/head.pyi b/nextpy/interfaces/web/components/base/head.pyi similarity index 97% rename from nextpy/frontend/components/base/head.pyi rename to nextpy/interfaces/web/components/base/head.pyi index ab52003e..efbbb8a1 100644 --- a/nextpy/frontend/components/base/head.pyi +++ b/nextpy/interfaces/web/components/base/head.pyi @@ -9,8 +9,8 @@ from typing import Any, Dict, Literal, Optional, Union, overload from nextpy.backend.vars import Var, BaseVar, ComputedVar from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style -from nextpy.frontend.components.component import Component, MemoizationLeaf +from nextpy.interfaces.web.style import Style +from nextpy.interfaces.web.components.component import Component, MemoizationLeaf class NextHeadLib(Component): @overload diff --git a/nextpy/frontend/components/base/link.py b/nextpy/interfaces/web/components/base/link.py similarity index 94% rename from nextpy/frontend/components/base/link.py rename to nextpy/interfaces/web/components/base/link.py index 1b1b7ac7..382e6b30 100644 --- a/nextpy/frontend/components/base/link.py +++ b/nextpy/interfaces/web/components/base/link.py @@ -5,7 +5,7 @@ from nextpy.backend.vars import Var -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.components.component import Component class RawLink(Component): diff --git a/nextpy/frontend/components/base/link.pyi b/nextpy/interfaces/web/components/base/link.pyi similarity index 98% rename from nextpy/frontend/components/base/link.pyi rename to nextpy/interfaces/web/components/base/link.pyi index 6d5801b5..67e18f57 100644 --- a/nextpy/frontend/components/base/link.pyi +++ b/nextpy/interfaces/web/components/base/link.pyi @@ -9,8 +9,8 @@ from typing import Any, Dict, Literal, Optional, Union, overload from nextpy.backend.vars import Var, BaseVar, ComputedVar from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.style import Style +from nextpy.interfaces.web.components.component import Component from nextpy.backend.vars import Var class RawLink(Component): diff --git a/nextpy/frontend/components/base/meta.py b/nextpy/interfaces/web/components/base/meta.py similarity index 92% rename from nextpy/frontend/components/base/meta.py rename to nextpy/interfaces/web/components/base/meta.py index 07f043a7..f8f2d215 100644 --- a/nextpy/frontend/components/base/meta.py +++ b/nextpy/interfaces/web/components/base/meta.py @@ -7,8 +7,8 @@ from typing import Optional -from nextpy.frontend.components.base.bare import Bare -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.components.base.bare import Bare +from nextpy.interfaces.web.components.component import Component class Title(Component): diff --git a/nextpy/frontend/components/base/meta.pyi b/nextpy/interfaces/web/components/base/meta.pyi similarity index 98% rename from nextpy/frontend/components/base/meta.pyi rename to nextpy/interfaces/web/components/base/meta.pyi index ee46ff60..17a002f9 100644 --- a/nextpy/frontend/components/base/meta.pyi +++ b/nextpy/interfaces/web/components/base/meta.pyi @@ -9,10 +9,10 @@ from typing import Any, Dict, Literal, Optional, Union, overload from nextpy.backend.vars import Var, BaseVar, ComputedVar from nextpy.backend.event import EventChain, EventHandler, EventSpec -from nextpy.frontend.style import Style +from nextpy.interfaces.web.style import Style from typing import Optional -from nextpy.frontend.components.base.bare import Bare -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.components.base.bare import Bare +from nextpy.interfaces.web.components.component import Component class Title(Component): def render(self) -> dict: ... diff --git a/nextpy/frontend/components/base/script.py b/nextpy/interfaces/web/components/base/script.py similarity index 90% rename from nextpy/frontend/components/base/script.py rename to nextpy/interfaces/web/components/base/script.py index 0e4959ae..3dbaa49d 100644 --- a/nextpy/frontend/components/base/script.py +++ b/nextpy/interfaces/web/components/base/script.py @@ -10,16 +10,16 @@ from typing import Any, Union from nextpy.backend.vars import Var -from nextpy.frontend.components.component import Component +from nextpy.interfaces.web.components.component import Component class Script(Component): """Next.js script component. - Note that this component differs from nextpy.frontend.components.base.document.NextScript + Note that this component differs from nextpy.interfaces.web.components.base.document.NextScript in that it is intended for use with custom and user-defined scripts. - It also differs from nextpy.frontend.components.base.link.ScriptTag, which is the plain + It also differs from nextpy.interfaces.web.components.base.link.ScriptTag, which is the plain HTML