From b5c04190082ed946dec7c290bdb9d543fd3175b8 Mon Sep 17 00:00:00 2001 From: ldevillez Date: Wed, 18 Sep 2024 15:23:08 +0200 Subject: [PATCH] ADD: export options for the stat module --- CHANGELOG.md | 1 + README.md | 5 ++ pyswtools/stat/definitions.py | 22 ++++++++ pyswtools/stat/main.py | 97 ++++++++++++++++++++++++++++++++--- pyswtools/utils.py | 34 ++++++++++++ 5 files changed, 152 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a72885..bfbe376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Adding the type of component into the display of the stat module - Adding a flag to filter the type of components - Adding the number of drawings using the parts +- Adding an option to export stats module to CSV or to clipboard ### Changed ### Deprecated ### Removed diff --git a/README.md b/README.md index 5b00403..d07f3ee 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,11 @@ You can select the type of compoments to include with `--filter`: - `part`: get only parts. Works only for list display - `assembly`: get only assemblies +You can select the export that you want with `--export`: +- `none`: (default) no export, only the display +- `csv`: generate a `bom.csv` in the same directory as the solidworks project +- `clipboard`: put the bom in the clipboard. You can copy paste it directly in excel + You can get only the elements with a default density (1000 Kg/m³) with the option `--only-default-density`. This option only works with a `type `list` ### Clean diff --git a/pyswtools/stat/definitions.py b/pyswtools/stat/definitions.py index d3ecaab..7a18d31 100644 --- a/pyswtools/stat/definitions.py +++ b/pyswtools/stat/definitions.py @@ -25,17 +25,36 @@ class TypeSort(str, Enum): NAME = "name" +class TypeExport(str, Enum): + """Class represeting an export type""" + + NONE = "none" + CSV = "csv" + CLIPBOARD = "clipboard" + + @dataclass class StatComponentTree: + """ + Class represting a component for a tree structure + """ + number: int children: list def dict(self): + """ + Convert the class to a dict structure + """ return {k: v for k, v in asdict(self).items()} @dataclass class StatComponent: + """ + Class representing a component for the stat module + """ + mass: float density: float number: int @@ -43,4 +62,7 @@ class StatComponent: numberDrawing: int def dict(self): + """ + Convert the class to a dict structure + """ return {k: v for k, v in asdict(self).items()} diff --git a/pyswtools/stat/main.py b/pyswtools/stat/main.py index 785a1b2..47b5266 100644 --- a/pyswtools/stat/main.py +++ b/pyswtools/stat/main.py @@ -6,11 +6,12 @@ import click # pylint: disable=relative-beyond-top-level -from ..utils import check_system, check_system_verbose -from ..helper_sw import open_app_and_file, is_temp, is_assembly, open_drawing +from ..utils import check_system, check_system_verbose, do_windows_clipboard +from ..helper_sw import open_app_and_file, is_temp, is_assembly from .definitions import ( TypeComponent, + TypeExport, TypeOutput, TypeSort, StatComponent, @@ -192,6 +193,36 @@ def remove_duplicate_conf_list(struct: dict, mass_struct: dict) -> None: del struct[name_test] +def sort_tulpe( + struct: list, mass_struct: dict, type_sort: TypeSort = TypeSort.MASS +) -> list: + """ + Sort a list of tuple struct following a given sort + The first element of a tuple is the name and the second is the structure + """ + + if type_sort is TypeSort.NAME: + return sorted( + struct, + key=lambda i: i[0], + reverse=True, + ) + if type_sort is TypeSort.MASS: + return sorted( + struct, + key=lambda i: mass_struct[i[0]].mass * i[1].number, + reverse=True, + ) + if type_sort is TypeSort.MASS_PART: + return sorted( + struct, + key=lambda i: mass_struct[i[0]].mass, + reverse=True, + ) + + return struct + + def sort_key_struct(struct: dict, type_sort: TypeSort = TypeSort.MASS) -> list: """ Sort a dict struct following a given sort @@ -238,6 +269,47 @@ def sort_key_struct_tree( return tree_struct.keys() +def export_struct( + struct, mass_struct, type_export: TypeExport, type_sort: TypeSort, root_dir: str +): + """ + export struct + """ + delim = "\t" + if type_export is TypeExport.CSV: + delim = ";" + cols = ["Name", "Mtot", "n", "Mpart", "Density", "Comp", "Drw"] + txt = delim.join(cols) + "\n" + stacks = sort_tulpe(list(struct.items()), mass_struct, type_sort) + + while len(stacks) > 0: + k, v = stacks.pop(0) + + values = [ + k, + f"{mass_struct[k].mass * v.number:.3f}", + str(v.number), + f"{mass_struct[k].mass:.3f}", + f"{mass_struct[k].density:.3f}", + str( + "Part" if mass_struct[k].typeComponent == TypeComponent.PART else "Ass." + ), + str(mass_struct[k].numberDrawing), + ] + txt += delim.join(values) + "\n" + + if hasattr(v, "children"): + stacks = ( + sort_tulpe(list(v.children.items()), mass_struct, type_sort) + stacks + ) + + if type_export is TypeExport.CLIPBOARD: + do_windows_clipboard(txt) + elif type_export is TypeExport.CSV: + with open(os.path.join(root_dir, "bom.csv"), "w+", encoding="utf8") as f: + f.write(txt) + + def print_header(): """ Display the header of the output @@ -433,6 +505,9 @@ def complete_info_assembly(sw_comp, dict_of_comp: dict) -> dict: @click.option( "--type-sort", "type_sort", type=click.Choice(TypeSort), default=TypeSort.MASS ) +@click.option( + "--export", "export", type=click.Choice(TypeExport), default=TypeExport.NONE +) @click.option( "--filter", "type_component", @@ -442,6 +517,7 @@ def complete_info_assembly(sw_comp, dict_of_comp: dict) -> dict: @click.option("--only-default-density", "only_default_density", is_flag=True) def stat( input_path: str, + export: TypeExport, type_output: TypeOutput, type_sort: TypeSort, type_component: TypeComponent, @@ -486,18 +562,25 @@ def stat( clean_confs(tree_of_comp, dict_of_comp) - print(tree_of_comp) - filter_component_type(tree_of_comp, dict_of_comp, type_component) + if only_default_density: + dict_of_comp = filter_density_list(dict_of_comp) + if type_output is TypeOutput.TREE: display_tree(tree_of_comp, dict_of_comp, type_sort) elif type_output is TypeOutput.LIST: - - if only_default_density: - dict_of_comp = filter_density_list(dict_of_comp) display_list(dict_of_comp, type_sort) else: click.echo( f"Type output {type_output} is unknown {type_output is TypeOutput.TREE} {type(type_output)}" ) + + if export is not TypeExport.NONE: + export_struct( + tree_of_comp if type_output is TypeOutput.TREE else dict_of_comp, + dict_of_comp, + export, + type_sort, + os.path.dirname(input_path), + ) diff --git a/pyswtools/utils.py b/pyswtools/utils.py index f060603..0b3eb87 100644 --- a/pyswtools/utils.py +++ b/pyswtools/utils.py @@ -4,6 +4,7 @@ import platform import click +import ctypes def check_system_verbose() -> bool: @@ -21,3 +22,36 @@ def check_system() -> bool: Check if the system is windows """ return platform.system() == "Windows" + + +def do_windows_clipboard(text): + """ + Put text into the clipboard + """ + # from http://pylabeditor.svn.sourceforge.net/viewvc/pylabeditor/trunk/src/shells.py?revision=82&view=markup + + cf_unicode_text = 13 + ghnd = 66 + + ctypes.windll.kernel32.GlobalAlloc.restype = ctypes.c_void_p + ctypes.windll.kernel32.GlobalLock.restype = ctypes.c_void_p + + buffer_size = (len(text) + 1) * 2 + h_global_mem = ctypes.windll.kernel32.GlobalAlloc( + ctypes.c_uint(ghnd), ctypes.c_size_t(buffer_size) + ) + lp_global_mem = ctypes.windll.kernel32.GlobalLock(ctypes.c_void_p(h_global_mem)) + ctypes.cdll.msvcrt.memcpy( + ctypes.c_void_p(lp_global_mem), + ctypes.c_wchar_p(text), + ctypes.c_int(buffer_size), + ) + ctypes.windll.kernel32.GlobalUnlock(ctypes.c_void_p(h_global_mem)) + if ctypes.windll.user32.OpenClipboard(0): + ctypes.windll.user32.EmptyClipboard() + ctypes.windll.user32.SetClipboardData( + ctypes.c_int(cf_unicode_text), ctypes.c_void_p(h_global_mem) + ) + ctypes.windll.user32.CloseClipboard() + return True + return False