diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md index 5082247c2..1d4bf3837 100644 --- a/docs/release-notes/index.md +++ b/docs/release-notes/index.md @@ -29,6 +29,15 @@ This is the list of changes to Taipy releases as they were published. [`taipy-templates` 4.0](https://pypi.org/project/taipy-templates/4.0.0/), and [`taipy-rest` 4.0](https://pypi.org/project/taipy-rest/4.0.0/) packages. +## Known issues + +

taipy-gui 4.0.0

+ +- The [**-H**](../userman/advanced_features/configuration/gui-config.md#p-port) short command line + option for setting the server hostname is broken.
+ Please use the full **--host** option instead.
+ This issue will be fixed in the next technical release for Taipy GUI. + ## New Features

taipy 4.0.0

@@ -262,6 +271,10 @@ additional features. ## New Features +- Authentication now supports + [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id) + including SSO and GUI integration.
+ TODO - Documentation is in progress. - Support for [Polars DataFrame Library](https://docs.pola.rs/).
Tabular data nodes (`CSVDataNode^`, `ParquetDataNode^`, `ExcelDataNode^`, `SQLTableDataNode^`, and `SQLDataNode^`) can now expose the data as Polars objects. They all support diff --git a/docs/tutorials/articles/using_tables/index.md b/docs/tutorials/articles/using_tables/index.md index 52c07d0f4..b478269be 100644 --- a/docs/tutorials/articles/using_tables/index.md +++ b/docs/tutorials/articles/using_tables/index.md @@ -103,8 +103,8 @@ You can customize the style of a table in Taipy using two properties: 1. *class_name*: This property allows you to apply a CSS class to the entire table. -2. *style*: With this property, you can apply a CSS class to specific rows, which you specify in - Python. +2. *row_class_name*: With this property, you can apply a CSS class to specific rows, which you + specify in Python. ### Property 1: class_name @@ -134,17 +134,17 @@ script. For example, if your Taipy application code is in `main.py`, your CSS co `main.css` in the same directory. You can find more details about this [here](../../../userman/gui/styling/index.md#style-sheets). -### Property 2: style +### Property 2: row_class_name -To have more precise control over our styling, we can utilize the style property of tables -to assign CSS classes to individual rows in the table. For instance, we can assign a -user-defined **highlight-row** CSS class to rows where the **Category** column is **Dessert** to +To have more precise control over our styling, we can utilize the *row_class_name* property of +the table control to assign CSS classes to individual rows in the table. For instance, we can assign +a user-defined **highlight-row** CSS class to rows where the **Category** column is **Dessert** to give them a yellow background. ![Property style](images/using_tables_3.png){width=50% : .tp-image-border } -The style property accepts a function. This function is applied to each row of the table and -returns a string specifying the CSS class to be used for that particular row. To create the +The *row_class_name* property accepts a function. This function is applied to each row of the table +and returns a string specifying the CSS class to be used for that particular row. To create the table mentioned above, you can use the following code: ```python title="main.py" @@ -153,11 +153,11 @@ def table_style(state, index, row): table_properties = { "class_name": "rows-bordered rows-similar", # optional - "style": table_style, + "row_class_name": table_style, } main_md = Markdown("<|{food_df}|table|properties=table_properties|>") -# or Markdown("<|{food_df}|table|class_name=rows-bordered rows-similar|style=table_style|>") +# or Markdown("<|{food_df}|table|class_name=rows-bordered rows-similar|row_class_name=table_style|>") ``` ```css @@ -170,25 +170,25 @@ main_md = Markdown("<|{food_df}|table|properties=table_properties|>") In our code, we also made use of the *class_name* property mentioned earlier, and we applied two Stylekit table classes: **rows-bordered** and **rows-similar**. The **rows-similar** class removes the default 0.5 opacity from odd rows. While it wasn't necessary, using it does enhance -the table's appearance when applying our **highlight-row** style. +the table's appearance when applying our **highlight-row** CSS class. ## Modifying Data Tables offer various properties for modifying data within the table. Notably, the *on_edit*, -*on_add*, and *on_delete* properties can receive user-defined **callback** functions. This -function is executed when you interact with the table, but it only appears when you specify the -relevant data modification property. +*on_add*, and *on_delete* properties can receive user-defined **callback** functions. These +functions are executed when you interact with the table. The interaction element appear when the +table is explicitly defined as *editable*. -Taipy doesn't come with default functions for these properties, so we'll define each function -ourselves to match our specific needs. We're also including the -[notify](../../../userman/gui/notifications.md) function within our data modification callback +Although Taipy comes with default callback function implementation for these properties, we will +define each function ourselves to match our specific needs. We're also including the +[*notify()*](../../../userman/gui/notifications.md) function within our data modification callback functions to send notifications to the user about their changes. ## Editing (*on_edit*) -When the *on_edit* property is used, new buttons with a pencil icon are added to each cell. -Clicking it allows the user to modify the value of that cell, -then clicking the tick triggers the callback function: +When the *editable* property is used, new buttons with a pencil icon are added to each cell. +Clicking it allows the user to modify the value of that cell. If *on_edit* is set to a callback +function, then clicking the tick triggers the callback function: ![Editing](images/tables-on_edit.gif){width=50% : .tp-image-border } @@ -208,7 +208,7 @@ def food_df_on_edit(state, var_name, payload): state.food_df = new_food_df notify(state, "I", f"Edited value from '{old_value}' to '{value}'. (index '{index}', column '{col}')") -main_md = Markdown("<|{food_df}|table|on_edit=food_df_on_edit|>") +main_md = Markdown("<|{food_df}|table|editable|on_edit=food_df_on_edit|>") ``` The table documentation provides more information on the function signature which is slightly @@ -221,9 +221,9 @@ we instead create a copy of the DataFrame, modify the relevant cell, then assign ## Adding (*on_add*) -Adding and deleting are quite similar to editing. When you specify the *on_add* property, a -`button` with a 'plus' icon is included, and when clicked, it triggers the defined *on_add* -callback function. +Adding and deleting are quite similar to editing. When *editable* is True, a +`button` with a 'plus' icon is included, and when clicked, it triggers the callback function +speficied in the *on_add* property. ![Adding](images/tables-on_add.gif){width=50% : .tp-image-border } @@ -236,7 +236,7 @@ def food_df_on_add(state, var_name, payload): notify(state, "S", f"Added a new row.") -main_md = Markdown("<|{food_df}|table|on_add=food_df_on_add|>") +main_md = Markdown("<|{food_df}|table|editable|on_add=food_df_on_add|>") ``` This code simply adds a new empty row to the top of the table (DataFrame). @@ -258,7 +258,7 @@ def food_df_on_delete(state, var_name, payload): state.food_df = state.food_df.drop(index=index) notify(state, "E", f"Deleted row at index '{index}'") -main_md = Markdown("<|{food_df}|table|on_delete=food_df_on_delete|>") +main_md = Markdown("<|{food_df}|table|editable|on_delete=food_df_on_delete|>") ``` ## Complete Code @@ -311,6 +311,7 @@ if __name__ == "__main__": table_properties = { "class_name": "rows-bordered", + "editable": True, "filter": True, "on_edit": food_df_on_edit, "on_delete": food_df_on_delete, diff --git a/mkdocs.yml_template b/mkdocs.yml_template index df5c725f4..fc13b22fe 100644 --- a/mkdocs.yml_template +++ b/mkdocs.yml_template @@ -387,6 +387,8 @@ extra: # to enable disqus, uncomment the following and put your disqus id below # disqus: disqus_id generator: false +exclude_docs: | + *.md_template # uncomment the following and put your google tracking id below to enable GA #google_analytics: #- UA-xxx diff --git a/tools/fetch_source_files.py b/tools/fetch_source_files.py index 547c9210c..a2d9f8e49 100644 --- a/tools/fetch_source_files.py +++ b/tools/fetch_source_files.py @@ -3,7 +3,11 @@ import shutil import subprocess -from _fetch_source_file import CLI, GitContext, read_doc_version_from_mkdocs_yml_template_file +from _fetch_source_file import ( + CLI, + GitContext, + read_doc_version_from_mkdocs_yml_template_file, +) # Assuming this script is in taipy-doc/tools TOOLS_PATH = os.path.dirname(os.path.abspath(__file__)) @@ -18,6 +22,9 @@ OPTIONAL_PACKAGES = {"gui": ["pyarrow", "pyngrok", "python-magic", "python-magic-bin"]} +# Ecosystem offering may have a different version than the main Taipy version +VERSION_MAP = {"designer": {"4.0": "1.2"}} + args = CLI(os.path.basename(__file__), REPOS).get_args() # Read version from mkdocs.yml template @@ -25,7 +32,8 @@ # Gather version information for each repository repo_defs = { - repo if repo == "taipy" else f"taipy-{repo}": {"version": "local", "tag": None} for repo in REPOS + PRIVATE_REPOS + repo if repo == "taipy" else f"taipy-{repo}": {"version": "local", "tag": None} + for repo in REPOS + PRIVATE_REPOS } CATCH_VERSION_RE = re.compile(r"(^\d+\.\d+?)(?:(\.\d+)(\..*)?)?|develop|local$") for version in args.version: @@ -62,13 +70,28 @@ repo_defs[repo]["version"] = version repo_defs[repo]["tag"] = tag +# Remap version if necessary +for repo, version_remap_desc in VERSION_MAP.items(): + repo_desc = repo_defs.get(repo, None) + if repo_desc is None: + repo = f"taipy-{repo}" + repo_desc = repo_defs.get(repo, None) + if repo_desc and ( + remapped_version := version_remap_desc.get(repo_desc["version"], None) + ): + repo_desc["version"] = remapped_version + # Test git, if needed git_command = "git" if args.no_pull and all(v["version"] == "local" for v in repo_defs.values()): git_command = None else: git_path = shutil.which(git_command) - if git_path is None or subprocess.run(f'"{git_path}" --version', shell=True, capture_output=True) is None: + if ( + git_path is None + or subprocess.run(f'"{git_path}" --version', shell=True, capture_output=True) + is None + ): raise IOError(f'Couldn\'t find command "{git_command}"') git_command = git_path @@ -91,14 +114,19 @@ elif version == "develop": with GitContext(repo, PRIVATE_REPOS): cmd = subprocess.run( - f'"{git_path}" ls-remote -q -h {github_root}{repo}.git', shell=True, capture_output=True, text=True + f'"{git_path}" ls-remote -q -h {github_root}{repo}.git', + shell=True, + capture_output=True, + text=True, ) if cmd.returncode: if repo in PRIVATE_REPOS or repo[6:] in PRIVATE_REPOS: repo_defs[repo]["skip"] = True continue else: - raise SystemError(f"Problem with {repo}:\nOutput: {cmd.stdout}\nError: {cmd.stderr}") + raise SystemError( + f"Problem with {repo}:\nOutput: {cmd.stdout}\nError: {cmd.stderr}" + ) else: with GitContext(repo, PRIVATE_REPOS): cmd = subprocess.run( @@ -112,9 +140,13 @@ repo_defs[repo]["skip"] = True continue else: - raise SystemError(f"Couldn't query branches from {loggable_github_root}{repo}.") + raise SystemError( + f"Couldn't query branches from {loggable_github_root}{repo}." + ) if f"release/{version}\n" not in cmd.stdout: - raise ValueError(f"No branch 'release/{version}' in repository '{repo}'.") + raise ValueError( + f"No branch 'release/{version}' in repository '{repo}'." + ) tag = repo_defs[repo]["tag"] if tag: cmd = subprocess.run( @@ -161,6 +193,7 @@ def safe_rmtree(dir: str): frontend_dir = os.path.join(ROOT_DIR, "taipy-fe") + # Fetch files def move_files(repo: str, src_path: str): # Read Pipfile dependency packages @@ -171,7 +204,9 @@ def move_files(repo: str, src_path: str): with open(pipfile_path, "r") as pipfile: while True: line = pipfile.readline() - if str(line) == "" or (reading_packages and (not line.strip() or line[0] == "[")): + if str(line) == "" or ( + reading_packages and (not line.strip() or line[0] == "[") + ): break line = line.strip() if line == "[packages]": @@ -181,7 +216,10 @@ def move_files(repo: str, src_path: str): if match and not match.group(1).startswith("taipy"): package = match.group(1).lower() version = match.group(2) - if repo_optional_packages is None or package not in repo_optional_packages: + if ( + repo_optional_packages is None + or package not in repo_optional_packages + ): if package in pipfile_packages: versions = pipfile_packages[package] if version in versions: @@ -197,37 +235,55 @@ def move_files(repo: str, src_path: str): for step_dir in [ step_dir for step_dir in os.listdir(gs_dir) - if step_dir.startswith("step_") and os.path.isdir(os.path.join(gs_dir, step_dir)) + if step_dir.startswith("step_") + and os.path.isdir(os.path.join(gs_dir, step_dir)) ]: safe_rmtree(os.path.join(gs_dir, step_dir)) for step_dir in [ step_dir for step_dir in os.listdir(src_path) - if step_dir.startswith("step_") and os.path.isdir(os.path.join(src_path, step_dir)) + if step_dir.startswith("step_") + and os.path.isdir(os.path.join(src_path, step_dir)) ]: - shutil.copytree(os.path.join(src_path, step_dir), os.path.join(gs_dir, step_dir)) + shutil.copytree( + os.path.join(src_path, step_dir), os.path.join(gs_dir, step_dir) + ) safe_rmtree(os.path.join(gs_dir, "src")) shutil.copytree(os.path.join(src_path, "src"), os.path.join(gs_dir, "src")) - shutil.copy(os.path.join(src_path, "index.md"), os.path.join(gs_dir, "index.md")) + shutil.copy( + os.path.join(src_path, "index.md"), os.path.join(gs_dir, "index.md") + ) saved_dir = os.getcwd() os.chdir(os.path.join(ROOT_DIR, "docs", "getting_started", repo[6:])) subprocess.run( - f"python {os.path.join(src_path, 'generate_notebook.py')}", shell=True, capture_output=True, text=True + f"python {os.path.join(src_path, 'generate_notebook.py')}", + shell=True, + capture_output=True, + text=True, ) os.chdir(saved_dir) elif repo == "taipy-designer": - designer_doc_dir = os.path.join(ROOT_DIR, "docs", "userman", "ecosystem", "designer") + designer_doc_dir = os.path.join( + ROOT_DIR, "docs", "userman", "ecosystem", "designer" + ) safe_rmtree(designer_doc_dir) src_documentation_dir = os.path.join(src_path, "documentation") saved_dir = os.getcwd() os.chdir(saved_dir) subprocess.run( - f"python {os.path.join(src_path, 'copy_examples.py')}", shell=True, capture_output=True, text=True + f"python {os.path.join(src_path, 'copy_examples.py')}", + shell=True, + capture_output=True, + text=True, ) os.chdir(saved_dir) - shutil.copytree(os.path.join(src_documentation_dir, "taipy_docs"), designer_doc_dir) - shutil.copy(os.path.join(src_documentation_dir, "mkdocs_taipy.yml"), - os.path.join(designer_doc_dir, "mkdocs.yml_template")) + shutil.copytree( + os.path.join(src_documentation_dir, "taipy_docs"), designer_doc_dir + ) + shutil.copy( + os.path.join(src_documentation_dir, "mkdocs_taipy.yml"), + os.path.join(designer_doc_dir, "mkdocs.yml_template"), + ) else: try: @@ -243,7 +299,9 @@ def copy(item: str, src: str, dst: str, rel_path: str): rel_path = f"{rel_path}/{item}" for sub_item in os.listdir(full_src): copy(sub_item, full_src, full_dst, rel_path) - elif any(item.endswith(ext) for ext in [".py", ".pyi", ".json", ".ipynb"]): + elif any( + item.endswith(ext) for ext in [".py", ".pyi", ".json", ".ipynb"] + ): if os.path.isfile(full_dst): # File exists - compare with open(full_src, "r") as f: src = f.read() @@ -274,9 +332,17 @@ def copy(item: str, src: str, dst: str, rel_path: str): if not os.path.isdir(frontend_dir): os.mkdir(frontend_dir) fe_src_dir = os.path.join(src_path, "frontend", "taipy-gui") - shutil.copytree(os.path.join(fe_src_dir, "src"), os.path.join(frontend_dir, "src")) - for f in [f for f in os.listdir(fe_src_dir) if f.endswith(".md") or f.endswith(".json")]: - shutil.copy(os.path.join(fe_src_dir, f), os.path.join(frontend_dir, f)) + shutil.copytree( + os.path.join(fe_src_dir, "src"), os.path.join(frontend_dir, "src") + ) + for f in [ + f + for f in os.listdir(fe_src_dir) + if f.endswith(".md") or f.endswith(".json") + ]: + shutil.copy( + os.path.join(fe_src_dir, f), os.path.join(frontend_dir, f) + ) finally: pass """ @@ -286,7 +352,10 @@ def copy(item: str, src: str, dst: str, rel_path: str): frontend_dir = os.path.join(ROOT_DIR, "taipy-fe") if os.path.isdir(os.path.join(frontend_dir, "node_modules")): - shutil.move(os.path.join(frontend_dir, "node_modules"), os.path.join(ROOT_DIR, "fe_node_modules")) + shutil.move( + os.path.join(frontend_dir, "node_modules"), + os.path.join(ROOT_DIR, "fe_node_modules"), + ) if os.path.isdir(os.path.join(frontend_dir)): shutil.rmtree(frontend_dir) @@ -300,7 +369,9 @@ def copy(item: str, src: str, dst: str, rel_path: str): if not args.no_pull: cwd = os.getcwd() os.chdir(src_path) - subprocess.run(f'"{git_path}" pull', shell=True, capture_output=True, text=True) + subprocess.run( + f'"{git_path}" pull', shell=True, capture_output=True, text=True + ) os.chdir(cwd) print(f" Copying from {src_path}...", flush=True) move_files(repo, src_path) @@ -320,7 +391,12 @@ def copy(item: str, src: str, dst: str, rel_path: str): # Checkout tag version saved_dir = os.getcwd() os.chdir(clone_dir) - subprocess.run(f'"{git_path}" checkout {tag}', shell=True, capture_output=True, text=True) + subprocess.run( + f'"{git_path}" checkout {tag}', + shell=True, + capture_output=True, + text=True, + ) os.chdir(saved_dir) move_files(repo, clone_dir) @@ -338,8 +414,13 @@ def handleRemoveReadonly(func, path, exc): shutil.rmtree(clone_dir, onerror=handleRemoveReadonly) -if os.path.isdir(os.path.join(ROOT_DIR, "fe_node_modules")) and os.path.isdir(os.path.join(frontend_dir)): - shutil.move(os.path.join(ROOT_DIR, "fe_node_modules"), os.path.join(frontend_dir, "node_modules")) +if os.path.isdir(os.path.join(ROOT_DIR, "fe_node_modules")) and os.path.isdir( + os.path.join(frontend_dir) +): + shutil.move( + os.path.join(ROOT_DIR, "fe_node_modules"), + os.path.join(frontend_dir, "node_modules"), + ) # Manually add the taipy.run() function. # TODO: Automate this, grabbing the function from the 'taipy' repository, @@ -395,10 +476,16 @@ def run(*services: t.Union[Gui, Rest, Orchestrator], **kwargs) -> t.Optional[t.U version = list(versions.keys())[0] if package == "modin": # Remove 'extras' from modin package requirements - version = re.sub(r"\{\s*extras.*?,\s*version\s*=\s*(.*?)\s*}", r"\1", version) + version = re.sub( + r"\{\s*extras.*?,\s*version\s*=\s*(.*?)\s*}", + r"\1", + version, + ) new_pipfile.write(f"{package} = {version}\n") if package not in legacy_pipfile_packages: - pipfile_changes.append(f"Package '{package}' added ({version})") + pipfile_changes.append( + f"Package '{package}' added ({version})" + ) elif legacy_pipfile_packages[package] != version: pipfile_changes.append( f"Package '{package}' version changed from " diff --git a/tools/postprocess.py b/tools/postprocess.py index e11fc6864..211eac940 100644 --- a/tools/postprocess.py +++ b/tools/postprocess.py @@ -31,7 +31,7 @@ def define_env(env): """ - Mandatory to make this a proper MdDocs macro + Mandatory to make this a proper MkDocs macro """ match = re.search(r"/en/(develop|(?:release-(\d+\.\d+)))/$", env.conf["site_url"]) env.conf["branch"] = ( @@ -108,24 +108,22 @@ def remove_dummy_h3(content: str, ids: Dict[str, str]) -> str: return content -def create_navigation_buttons(site_url: str) -> str: - def create_button( - site_url: str, label: str, relative_url: str, class_name: str, group: str = "" - ) -> str: +def create_navigation_buttons() -> str: + def create_button(label: str, path: str, class_name: str, group: str = "") -> str: gclass = " .tp-nav-button-group_element" if group else "" html = f""" - +

{label}

""" if group == "start-group": - return "
" + html + return '
' + html elif group == "end-group": - return html + "
" + return html + "
" else: - return "
" + html + "
" + return '
' + html + "
" buttons_html = """
@@ -137,17 +135,12 @@ def create_button( "Visual Elements", "refmans/gui/viselements/", "tp-content-card--beta", - "start-group" - ), - ( - "Reference", - "refmans/", - "tp-content-card--beta", - "end-group" + "start-group", ), + ("Reference", "refmans/", "tp-content-card--beta", "end-group"), ("Gallery", "gallery/", "tp-content-card--alpha"), ]: - buttons_html += create_button(site_url, *desc) + buttons_html += create_button(*desc) buttons_html += """
""" @@ -160,7 +153,9 @@ def on_post_build(env): """ log = logging.getLogger("mkdocs") + site_dir = env.conf["site_dir"] + site_dir_unix = site_dir.replace("\\", "/") site_url = env.conf["site_url"] xrefs = {} multi_xrefs = {} @@ -182,14 +177,12 @@ def on_post_build(env): multi_xrefs[xref] = sorted(descs, key=lambda p: len(p[0])) ref_files_path = os.path.join(site_dir, "refmans", "reference") fixed_cross_refs = {} - navigation_buttons = create_navigation_buttons(site_url) + # Create navigation button once for all pages + navigation_buttons = create_navigation_buttons() for root, _, file_list in os.walk(site_dir): for f in file_list: - # Remove the *_template files - if f.endswith("_template"): - os.remove(os.path.join(root, f)) # Post-process generated '.html' files - elif f.endswith(".html"): + if f.endswith(".html"): filename = os.path.join(root, f) with open(filename) as html_file: try: @@ -197,19 +190,32 @@ def on_post_build(env): except Exception as e: log.error(f"Couldn't read HTML file {filename}") raise e - + # Remove useless spaces for improved processing # This breaks the code blocks - so needs to avoid the
 elements before
                     # we bring it back.
-                    #html_content = re.sub(r"[ \t]+", " ", re.sub(r"\n\s*\n+", "\n\n", html_content))
-                    #html_content = html_content.replace("\n\n", "\n")
+                    # html_content = re.sub(r"[ \t]+", " ", re.sub(r"\n\s*\n+", "\n\n", html_content))
+                    # html_content = html_content.replace("\n\n", "\n")
 
                     html_content = html_content.replace(
                         '