scilens 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. scilens/LICENSE +37 -0
  2. scilens/__init__.py +1 -0
  3. scilens/app.py +9 -0
  4. scilens/cli/__init__.py +0 -0
  5. scilens/cli/cglb.py +10 -0
  6. scilens/cli/config.py +15 -0
  7. scilens/cli/info.py +12 -0
  8. scilens/cli/main.py +74 -0
  9. scilens/components/analyse_folder.py +4 -0
  10. scilens/components/compare_2_files.py +22 -0
  11. scilens/components/compare_errors.py +22 -0
  12. scilens/components/compare_floats.py +39 -0
  13. scilens/components/compare_folders.py +39 -0
  14. scilens/components/executor.py +58 -0
  15. scilens/components/file_reader.py +29 -0
  16. scilens/config/__init__.py +0 -0
  17. scilens/config/cli_run_options.py +8 -0
  18. scilens/config/env_var.py +3 -0
  19. scilens/config/load.py +40 -0
  20. scilens/config/models/__init__.py +12 -0
  21. scilens/config/models/app.py +9 -0
  22. scilens/config/models/compare.py +5 -0
  23. scilens/config/models/compare_float_thresholds.py +4 -0
  24. scilens/config/models/execute.py +4 -0
  25. scilens/config/models/execute_and_compare.py +4 -0
  26. scilens/config/models/file_reader.py +3 -0
  27. scilens/config/models/reader_format_csv.py +18 -0
  28. scilens/config/models/reader_format_txt.py +5 -0
  29. scilens/config/models/readers.py +4 -0
  30. scilens/config/models/report.py +6 -0
  31. scilens/config/models/report_html.py +6 -0
  32. scilens/config/models/report_output.py +4 -0
  33. scilens/helpers/assets.py +27 -0
  34. scilens/helpers/search_and_index.py +26 -0
  35. scilens/helpers/templates/index.html +20 -0
  36. scilens/helpers/templates/style.css +124 -0
  37. scilens/processors/__init__.py +3 -0
  38. scilens/processors/analyse.py +14 -0
  39. scilens/processors/compare.py +7 -0
  40. scilens/processors/execute_and_compare.py +17 -0
  41. scilens/processors/models/__init__.py +0 -0
  42. scilens/processors/models/results.py +2 -0
  43. scilens/processors/processor_interface.py +9 -0
  44. scilens/readers/__init__.py +0 -0
  45. scilens/readers/exceptions.py +1 -0
  46. scilens/readers/reader_com_txt_lines.py +2 -0
  47. scilens/readers/reader_csv.py +73 -0
  48. scilens/readers/reader_interface.py +15 -0
  49. scilens/readers/reader_manager.py +30 -0
  50. scilens/readers/reader_txt.py +82 -0
  51. scilens/readers/transform.py +8 -0
  52. scilens/report/__init__.py +0 -0
  53. scilens/report/assets/logo.svg +9 -0
  54. scilens/report/assets/logo_cglb.svg +1 -0
  55. scilens/report/assets.py +11 -0
  56. scilens/report/html_report.py +26 -0
  57. scilens/report/report.py +30 -0
  58. scilens/report/template.py +8 -0
  59. scilens/report/templates/body_01_title.html +61 -0
  60. scilens/report/templates/body_99_footer.html +11 -0
  61. scilens/report/templates/compare_11_summary.html +75 -0
  62. scilens/report/templates/compare_12_sections.html +150 -0
  63. scilens/report/templates/compare_13_section_numbers copy.html +48 -0
  64. scilens/report/templates/compare_13_section_numbers.html +91 -0
  65. scilens/report/templates/index.html +92 -0
  66. scilens/report/templates/js_chartlibs_echarts.js +34 -0
  67. scilens/report/templates/js_chartlibs_plotly.js +55 -0
  68. scilens/report/templates/js_com_dom.js +21 -0
  69. scilens/report/templates/js_com_page.js +199 -0
  70. scilens/report/templates/js_com_palette.js +7 -0
  71. scilens/report/templates/js_curvemgr.js +319 -0
  72. scilens/report/templates/style.css +144 -0
  73. scilens/report/templates/utils_compare_report_anim.js +73 -0
  74. scilens/report/templates/utils_compare_report_framesseries.js +143 -0
  75. scilens/report/vendors/tailwindcss_3_4_15.js +83 -0
  76. scilens/report/vendors/tailwindcss_min_4.0.0-beta.4.js +26 -0
  77. scilens/run/__init__.py +0 -0
  78. scilens/run/models/task_results.py +5 -0
  79. scilens/run/models/task_runtime.py +2 -0
  80. scilens/run/run_task.py +41 -0
  81. scilens/run/standalone_task_runner.py +9 -0
  82. scilens/run/task_context.py +12 -0
  83. scilens/run/tasks_collector.py +52 -0
  84. scilens/utils/dict.py +6 -0
  85. scilens/utils/file.py +43 -0
  86. scilens/utils/php.py +15 -0
  87. scilens/utils/system.py +2 -0
  88. scilens/utils/template.py +5 -0
  89. scilens/utils/time_tracker.py +6 -0
  90. scilens/utils/vectors.py +4 -0
  91. scilens/utils/web.py +20 -0
  92. scilens-0.1.0.dist-info/METADATA +44 -0
  93. scilens-0.1.0.dist-info/RECORD +95 -0
  94. scilens-0.1.0.dist-info/WHEEL +4 -0
  95. scilens-0.1.0.dist-info/entry_points.txt +3 -0
scilens/LICENSE ADDED
@@ -0,0 +1,37 @@
1
+ Licence d'Utilisation scilens
2
+
3
+ 1. Objet de la Licence
4
+
5
+ Cette licence définit les termes et conditions d'utilisation de la bibliothèque logicielle appelée `scilens` (ci-après désignée `la Bibliothèque`).
6
+
7
+ 2. Droits d'utilisation
8
+
9
+ - L'achat ou la réception d'une licence accorde à l'utilisateur une licence limitée, non exclusive, et non transférable pour utiliser la Bibliothèque conformément aux termes de cette licence.
10
+ - L'utilisateur est autorisé à utiliser la Bibliothèque uniquement pour des projets dans le cadre défini par l'accord.
11
+
12
+ 3. Restrictions
13
+
14
+ L'utilisateur s'engage à respecter les restrictions suivantes :
15
+
16
+ - Interdiction de redistribution : Il est strictement interdit de distribuer, vendre, louer, ou prêter la Bibliothèque à des tiers, sous quelque forme que ce soit.
17
+ - Code source : La Bibliothèque est fournie sous forme binaire ou compilée, sauf disposition contraire. Toute tentative de décompilation, rétro-ingénierie, ou modification est strictement interdite.
18
+ - Propriété intellectuelle : Tous les droits de propriété intellectuelle relatifs à la Bibliothèque sont et restent la propriété exclusive de CesGensLaB.
19
+
20
+ 4. Limitations de garantie
21
+
22
+ - La Bibliothèque est fournie "en l'état" sans garantie expresse ou implicite.
23
+ - CesGensLaB ne pourra être tenu responsable de tout dommage direct ou indirect, incluant mais non limité à la perte de données, la perte de profits ou l'interruption de service.
24
+
25
+ 5. Durée et résiliation
26
+
27
+ - Cette licence est valable jusqu'à sa résiliation.
28
+ - Elle peut être résiliée automatiquement en cas de violation des termes de cette licence.
29
+ - À la résiliation, l'utilisateur doit détruire toutes les copies de la Bibliothèque en sa possession.
30
+
31
+ 6. Droit applicable
32
+
33
+ Cette licence est régie par les lois françaises et tout litige sera soumis à la compétence exclusive des tribunaux français.
34
+
35
+ ---
36
+
37
+ Contact : Pour toute question relative à cette licence, veuillez contacter legal@cesgenslab.fr.
scilens/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from.run.standalone_task_runner import StandaloneTaskRunner
scilens/app.py ADDED
@@ -0,0 +1,9 @@
1
+ import importlib.metadata
2
+ pkg_name='scilens'
3
+ product_name='SciLens'
4
+ pkg_version=importlib.metadata.version(pkg_name)
5
+ pkg_homepage=importlib.metadata.metadata(pkg_name).get('home-page')
6
+ pkg_documentations_url=None
7
+ try:pkg_documentations_url=importlib.metadata.metadata(pkg_name)['Project-URL'].split(', ')[1]
8
+ except Exception:pass
9
+ powered_by={'name':'CesGensLab','url':'https://cesgenslab.cloud'}
File without changes
scilens/cli/cglb.py ADDED
@@ -0,0 +1,10 @@
1
+ import time,rich_click as click
2
+ from rich.console import Console
3
+ from rich.progress import Progress
4
+ def cglb_process():
5
+ B='results';A='command';G=Console();E=[{A:'dev',B:'better'},{A:'build',B:'bigger'},{A:'run',B:'faster'}]
6
+ for C in E:
7
+ with Progress()as D:
8
+ F=D.add_task(f"[cyan]{C[A]}".ljust(20,'-'),total=10)
9
+ for H in range(10):time.sleep(.1);D.update(F,advance=1)
10
+ click.echo(click.style(' => '+C[B].upper()+' !\n',fg='magenta'))
scilens/cli/config.py ADDED
@@ -0,0 +1,15 @@
1
+ import json,click
2
+ from rich.console import Console
3
+ from rich.syntax import Syntax
4
+ from scilens.app import pkg_name,pkg_version
5
+ from scilens.config.models import AppConfig
6
+ from scilens.config.load import pydantic_to_yaml
7
+ from scilens.config.env_var import get_vars
8
+ console=Console()
9
+ def config_print(mode):
10
+ H='green';G='json';F='yaml';B=mode;A=' '
11
+ if B==F:I=pydantic_to_yaml(AppConfig(processor='processor'));C=Syntax(I,F,line_numbers=True);console.print();console.rule(f"[bold green]{pkg_name}.yml[/bold green]");console.print(C)
12
+ if B==G:J=AppConfig.model_json_schema();C=Syntax(json.dumps(J,indent=2),G,line_numbers=True);console.print(C)
13
+ if B=='envvars':
14
+ D=get_vars();E=max([len(A)for A in D])+4;K=max([len(A)for A in D.values()])+4;click.echo(click.style(('Environment variable name'+A).ljust(E,A)+A+'Config variable path',fg=H));click.echo(click.style('='*E+A+'='*K,fg=H))
15
+ for(L,M)in D.items():click.echo(click.style((L+A).ljust(E,'.'),fg='yellow')+A+M)
scilens/cli/info.py ADDED
@@ -0,0 +1,12 @@
1
+ _A='yellow'
2
+ import rich_click as click
3
+ from scilens.app import pkg_name,pkg_version,pkg_homepage,pkg_documentations_url,powered_by
4
+ from scilens.utils.system import info as system_info
5
+ def echo_draw():click.echo(click.style(" \n ::::::::: _____ _ _ \n ::::::::::: / ____| (_) | | \n :: ::::::::::: | (___ ___ _ | | ___ _ __ ___ \n :::: :::::: ::::::::::::: \\___ \\ / __| | | | | / _ \\ | '_ \\ / __| \n :::: :::::: :::::::::::::: ____) | | (__ | | | |____ | __/ | | | | \\__ \\ \n :::::: ::: ::::::::::::::::: |_____/ \\___| |_| |______| \\___| |_| |_| |___/ \n :::::::::::::::::::::::::::::::: \n ::::::::::::::::::::::::::::::::: \n :::::::::::::::::::::::::::::::::: \n ::::::::::::::: \n ::::::::::::: \n :::::::::::: \n :::: :::::::::::: \n ::::::: ::::::::::: \n :::::::: ::::::::::: \n :::: :: ::::::::::: \n :::::::::::::::::::: \n ::::::::::::::: \n :::::::::: \n\n \n",fg='magenta'))
6
+ def echo_info():
7
+ B='blue';A='black';click.echo(pkg_name+' '+click.style(f"v{pkg_version}",fg=_A));click.echo(click.style('Visit ...........',fg=A)+' '+click.style(pkg_homepage,fg=B))
8
+ if pkg_documentations_url:click.echo(click.style('Documentations ..',fg=A)+' '+click.style(pkg_documentations_url,fg=B))
9
+ click.echo(click.style('Powered by ......',fg=A)+' '+click.style(powered_by['url'],fg=B))
10
+ def echo_system_info():
11
+ A=system_info()
12
+ for(B,C)in A.items():click.echo((B+' ').ljust(15,'.')+' '+click.style(f"{C}",fg=_A))
scilens/cli/main.py ADDED
@@ -0,0 +1,74 @@
1
+ _A=True
2
+ import logging,os,coloredlogs,rich_click as click
3
+ from scilens.app import pkg_name,pkg_version
4
+ from scilens.readers.reader_manager import ReaderManager
5
+ from scilens.run.tasks_collector import TasksCollector
6
+ from scilens.run.models.task_results import TaskResults
7
+ from scilens.utils.time_tracker import TimeTracker
8
+ from scilens.cli.config import config_print
9
+ from scilens.cli.cglb import cglb_process
10
+ from scilens.cli.info import echo_info,echo_system_info,echo_draw
11
+ from scilens.config.cli_run_options import get_vars
12
+ from scilens.helpers.search_and_index import SearchAndIndex
13
+ def echo_separator(msg=None):
14
+ B='green';A=msg
15
+ if A:A=f" {A}";click.echo(click.style(A.rjust(80,'='),fg=B))
16
+ else:click.echo(click.style('='*80,fg=B))
17
+ def echo_task_error():click.echo(click.style('='*80,fg='red'))
18
+ @click.group()
19
+ @click.option('--log-level',default='INFO',help='Log level. Default is INFO. Available levels are DEBUG, INFO, WARNING, ERROR, CRITICAL.')
20
+ def cli(log_level):coloredlogs.install(level=log_level)
21
+ @cli.command(short_help='Show the version information')
22
+ def version():click.echo(f"{pkg_name} v{pkg_version}")
23
+ @cli.command(short_help='CesGensLaB Slogan',hidden=_A)
24
+ def cesgenslab():cglb_process()
25
+ @cli.command(short_help='Show application informations')
26
+ def info():echo_draw();echo_info()
27
+ @cli.command(short_help='Show system informations. (Useful for dynamic context configuration)')
28
+ def sysinfo():echo_system_info()
29
+ @cli.command(short_help='Show the reader plugins')
30
+ def reader():
31
+ for A in ReaderManager()._get_plugin_names():click.echo(click.style(A,fg='yellow'))
32
+ @cli.group()
33
+ def config():0
34
+ @config.command(name='default',short_help='Example of default config')
35
+ def config_default():config_print('yaml')
36
+ @config.command(name='json',short_help='Show the config structure in json schema format')
37
+ def config_json():config_print('json')
38
+ @config.command(name='envvar',short_help='Show the available environment variables')
39
+ def config_json():config_print('envvars')
40
+ @cli.command(short_help='Run data collections and analysis')
41
+ @click.argument('path',required=_A)
42
+ @click.option('--config',help=f'Path to the yaml test configuration file. If not provided, search for a file named "{pkg_name}.yml" in the PATH (exlusive with --processor, --collect-depth and --collect-discover).')
43
+ @click.option('--collect-discover',is_flag=_A,help=f'Explore recursively PATH seeking for "{pkg_name}.yml" configuration files, and run them sequentially (exlusive with --config and --processor and --collect-depth).')
44
+ @click.option('--collect-depth',type=int,help=f'Walk the PATH and list subdirectories at depth --collect-depth." (exlusive with --config, --processor and --collect-discover).')
45
+ @click.option('--processor',help=f"If specified, run with this processor, default configuration values, and environment variables (exlusive with --config, --collect-depth and --collect-discover).")
46
+ @click.option('--tag',multiple=_A,help=f'Filter collected tasks on tags defined in corresponding "{pkg_name}.yml". (Can be used multiple times).')
47
+ @click.option('--collect-only',is_flag=_A,help=f"Collect only, with --discover or not. Do not run the tasks.")
48
+ @click.option('--report-title',help=f"Define report title.")
49
+ @click.option('--export-html',is_flag=_A,help=f"Generate HTML report(s).")
50
+ @click.option('--export-json',is_flag=_A,help=f"Generate JSON report(s).")
51
+ @click.option('--export-yaml',is_flag=_A,help=f"Generate YAML report(s).")
52
+ @click.option('--export-html-add-index',is_flag=_A,help=f"Generate an index for generated HTML reports.")
53
+ def run(path,config,collect_discover,collect_depth,processor,tag,collect_only,report_title,export_html,export_json,export_yaml,export_html_add_index):
54
+ R='datetime';K=processor;J=collect_discover;I=config;E=collect_depth;echo_separator('Info');echo_info();echo_separator('System Info');echo_system_info();S=list(tag);B=os.path.abspath(path)
55
+ if not os.path.isdir(B):logging.error(f"Dir '{B}' does not exist.");exit(1)
56
+ if not bool(I)+J+(bool(E)or E==0)+bool(K)in[0,1]:logging.error(f"Options --config, --collect-discover, --collect-depth and --processor are mutually exclusive.");exit(1)
57
+ echo_separator('Collecting tasks');F=[]
58
+ try:F=TasksCollector(B,I,J,E,S,K).process(get_vars(report_title,export_html,export_json,export_yaml))
59
+ except Exception as D:logging.error(D);exit(1)
60
+ if collect_only:echo_separator();logging.info('Collecting tasks only - End');echo_separator();exit(0)
61
+ else:
62
+ L=TimeTracker();A=len(F);M=0;N=0;O=0;P=[]
63
+ for(T,U)in enumerate(F):
64
+ echo_separator(f"Running task {T+1}/{A}")
65
+ try:C=U.process()
66
+ except Exception as D:echo_task_error();logging.error(D);C=TaskResults(error=str(D))
67
+ if C.report_results:P+=C.report_results.files_created
68
+ if C.error:M+=1
69
+ G=C.processor_results
70
+ if G:
71
+ if G.warnings:O+=1
72
+ if G.errors:N+=1
73
+ if export_html_add_index:echo_separator(f"Post Actions");logging.info(f"Generate HTML Index for the HTML reports");V=[A for A in P if A.endswith('.html')];Q=os.path.join(B,'index.html');SearchAndIndex().create_html_index(SearchAndIndex().file_list_info_from_origin(V,B),Q,title='Index of Reports');logging.info(f"HTML Index generated at '{Q}'")
74
+ echo_separator(f"End Running tasks ({A}/{A})");L.stop();H=L.get_data();logging.info(f"Tasks with errors ............... {M}/{A}");logging.info(f"Tasks with processor errors ..... {N}/{A}");logging.info(f"Tasks with processor warnings ... {O}/{A}");logging.info(f"Start ........................... {H['start'][R]} utc");logging.info(f"End ............................. {H['end'][R]} utc");logging.info(f"Duration......................... {H['duration_seconds']} seconds");echo_separator()
@@ -0,0 +1,4 @@
1
+ import os
2
+ class AnalyseFolder:
3
+ def __init__(A):0
4
+ def get_list_dir(C,dir,exclude_files=None):A=exclude_files;B=[os.path.join(dir,A)for A in os.listdir(dir)];return B if not A else[B for B in B if B not in A]
@@ -0,0 +1,22 @@
1
+ import os
2
+ from scilens.run.task_context import TaskContext
3
+ from scilens.components.file_reader import FileReader
4
+ from scilens.components.compare_errors import CompareErrors,SEVERITY_ERROR
5
+ from scilens.components.compare_floats import CompareFloats
6
+ class Compare2Files:
7
+ def __init__(A,context):A.context=context
8
+ def compare(B,path_test,path_ref):
9
+ Q='comparison_errors';P='comparison';M='reader';J='error';I='ref';H='test';E='path';A={H:{},I:{},P:None,Q:None};D={H:{E:path_test},I:{E:path_ref}}
10
+ for(K,F)in D.items():
11
+ if not F.get(E)or not os.path.exists(F[E]):A[J]=f"file {K} does not exist";return A
12
+ R=FileReader(B.context.working_dir,B.context.config.file_reader,B.context.config.readers,config_alternate_path=B.context.origin_working_dir)
13
+ for(K,F)in D.items():D[K][M]=R.read(F[E])
14
+ C=D[H][M];G=D[I][M]
15
+ if not C or not G:A['skipped']=True;return A
16
+ A[H]=C.info();A[I]=G.info()
17
+ if C.read_error:A[J]=C.read_error;return A
18
+ L=CompareErrors(B.context.config.compare.errors_limit,B.context.config.compare.ignore_warnings);N,S=C.compare(CompareFloats(L,B.context.config.compare.float_thresholds),G,param_is_ref=True);A[P]=S;A[Q]=L.get_data()
19
+ if N:A[J]=N;return A
20
+ C.close();G.close();O=len(L.errors[SEVERITY_ERROR])
21
+ if O>0:T=f"{O} comparison errors";A[J]=T
22
+ return A
@@ -0,0 +1,22 @@
1
+ _B=False
2
+ _A=None
3
+ from pydantic import BaseModel
4
+ class CompareGroup(BaseModel):name:str;data:dict|_A
5
+ class CompareErrFloats(BaseModel):is_relative:bool;value:float;test:float;reference:float
6
+ class CompareErr(BaseModel):err:CompareErrFloats|_A;msg:int;group:int|_A=_A;info:dict|_A=_A
7
+ SEVERITY_ERROR='error'
8
+ SEVERITY_WARNING='warning'
9
+ class CompareErrors:
10
+ def __init__(A,nb_max,ignore_warnings=_B):A.nb_max=nb_max;A.ignore_warnings=ignore_warnings;A.errors={SEVERITY_ERROR:[],SEVERITY_WARNING:[]};A.count=0;A.limit_reached=_B;A.messages=[];A.messages_map={};A.groups=[]
11
+ def add_group(A,name,data=_A):B=len(A.groups);A.groups.append(CompareGroup(name=name,data=data));return B
12
+ def add(A,severity,message,comp_err,group_idx=_A,info=_A):
13
+ D=severity;C=message
14
+ if A.ignore_warnings and D==SEVERITY_WARNING:return _B
15
+ A.count+=1;B=A.messages_map.get(C)
16
+ if B is _A:B=len(A.messages);A.messages.append(C);A.messages_map[C]=B
17
+ A.errors[D].append(CompareErr(err=comp_err,msg=B,group=group_idx,info=info))
18
+ if A.count>=A.nb_max:A.limit_reached=True;return True
19
+ def get_data(B):
20
+ A={'messages':B.messages,'groups':[A.model_dump()for A in B.groups]}
21
+ for(C,D)in B.errors.items():A[C]=[A.model_dump()for A in D];A[C+'_nb']=len(A[C])
22
+ return A
@@ -0,0 +1,39 @@
1
+ _C='amplitude'
2
+ _B=False
3
+ _A=None
4
+ from scilens.components.compare_errors import CompareErrors,SEVERITY_ERROR,SEVERITY_WARNING,CompareErrFloats
5
+ from scilens.config.models import CompareFloatThresholdsConfig
6
+ def vector_get_amplitude(vector):A=vector;B=min(A);C=max(A);return{'min':B,'max':C,_C:abs(C-B)}
7
+ class CompareFloats:
8
+ def __init__(A,compare_errors,config):A.compare_errors=compare_errors;A.thresholds=config
9
+ def compare_vectors(B,test_vector,reference_vector,group_idx=_A,info_vector=_A):
10
+ H=info_vector;E=reference_vector;C=test_vector;I=0;J={SEVERITY_ERROR:0,SEVERITY_WARNING:0};K=_B;F=_A
11
+ if B.thresholds.vectors.amplitude_moderation:P=vector_get_amplitude(C)[_C];F=P*B.thresholds.vectors.amplitude_moderation.multiplier;L=B.thresholds.vectors.amplitude_moderation.method
12
+ Q=len(C)
13
+ for A in range(Q):
14
+ M=C[A]-E[A]
15
+ if M==0:continue
16
+ else:I+=1
17
+ if K:continue
18
+ if F is not _A and abs(M)<F:
19
+ if L=='ignore':continue
20
+ elif L=='soften':D,N,G=B.compare_2_values(C[A],E[A]);D=SEVERITY_WARNING
21
+ else:D,N,G=B.compare_2_values(C[A],E[A])
22
+ if G:
23
+ J[D]+=1;O={'index':A}
24
+ if H:O['info']=H[A]
25
+ K=B.compare_errors.add(D,N,G,group_idx=group_idx,info=O)
26
+ return _B,I,J
27
+ def compare_2_values(G,test,reference):
28
+ D=test;B=reference;A=G.thresholds;F=-1 if D-B<0 else 1
29
+ if abs(D)>A.relative_vs_absolute_min and B!=0:
30
+ C=abs(D-B)/abs(B);E=CompareErrFloats(is_relative=True,value=F*C,test=D,reference=B)
31
+ if C<A.relative_error_max:
32
+ if C>A.relative_error_min:return SEVERITY_WARNING,f"Rel. err. > {A.relative_error_min} and < {A.relative_error_max}",E
33
+ else:return SEVERITY_ERROR,f"Rel. err. > {A.relative_error_max}",E
34
+ else:
35
+ C=abs(D-B);E=CompareErrFloats(is_relative=_B,value=F*C,test=D,reference=B)
36
+ if C<A.absolute_error_max:
37
+ if C>A.absolute_error_min:return SEVERITY_WARNING,f"Abs. err. > {A.absolute_error_min} and < {A.absolute_error_max}",E
38
+ else:return SEVERITY_ERROR,f"Abs. err. > {A.absolute_error_max}",E
39
+ return _A,_A,_A
@@ -0,0 +1,39 @@
1
+ _E='ref'
2
+ _D='test'
3
+ _C='name'
4
+ _B=None
5
+ _A='path'
6
+ import logging,os
7
+ from scilens.run.task_context import TaskContext
8
+ from scilens.components.compare_2_files import Compare2Files
9
+ def list_dir(path,filename_match_ignore,recursive,exclude_filepaths=_B):
10
+ I=exclude_filepaths;H=filename_match_ignore;G='rel_path';F='filename_clean';B='';A=path
11
+ if recursive:
12
+ C=[]
13
+ for(D,L,K)in os.walk(A):
14
+ for J in K:C.append({_A:os.path.join(D,J),F:J.replace(str(H),B),G:D.replace(A+os.path.sep,B)if D!=A else B})
15
+ E={os.path.join(A[G],A[F]):A for A in C}
16
+ else:C={C.replace(str(H),B):C for C in os.listdir(A)if os.path.isfile(os.path.join(A,C))};E={C:{_A:os.path.join(A,D),F:C,G:B}for(C,D)in C.items()}
17
+ return{B:A for(B,A)in E.items()if A[_A]not in I}if I else E
18
+ class CompareFolders:
19
+ def __init__(A,context):
20
+ C=context;A.context=C;B=C.config.compare.sources;A.cfg=B;A.test_base=os.path.join(C.working_dir,B.test_folder_relative_path);A.ref_base=os.path.join(C.working_dir,B.reference_folder_relative_path)
21
+ if B.additional_path_suffix:A.test=os.path.join(A.test_base,B.additional_path_suffix);A.ref=os.path.join(A.ref_base,B.additional_path_suffix)
22
+ else:A.test=A.test_base;A.ref=A.ref_base
23
+ def compute_list_filenames(A):
24
+ logging.info(f"Comparing folders content: test vs reference");logging.debug(f"Comparing folders content: {A.test} vs {A.ref}");E=[A.context.config_file]if A.context.config_file else _B;F=[]
25
+ if A.test!=A.ref:
26
+ logging.info(f"Listing files in test folder");logging.debug(f"-- test folder: {A.test}");C=list_dir(A.test,A.cfg.test_filename_match_ignore,A.cfg.recursive,exclude_filepaths=E);logging.info(f"Listing files in reference folder");logging.debug(f"-- test reference: {A.ref}");D=list_dir(A.ref,A.cfg.reference_filename_match_ignore,A.cfg.recursive,exclude_filepaths=E);G=sorted(list(set(C.keys())|set(D.keys())))
27
+ for B in G:F.append({_C:B,_D:C[B][_A]if C.get(B)else _B,_E:D[B][_A]if D.get(B)else _B})
28
+ return F
29
+ def compute_comparison(E,items):
30
+ C='error';D=[]
31
+ for B in items:
32
+ logging.info(f"Comparing file: {B[_C]}")
33
+ try:A=Compare2Files(E.context).compare(B[_D],B[_E])
34
+ except Exception as F:A={C:str(F),_D:{},_E:{}}
35
+ A[_C]=B[_C];D.append(A)
36
+ if A.get(C):
37
+ logging.warning(f"Error found in comparison: {A[C]}")
38
+ if A[C]=='No reader found':logging.warning(f"Maybe Config Options could used to derive the correct reader or skip the file");logging.warning(f" - file_reader.extension_unknown_ignore to skip");logging.warning(f" - file_reader.extension_fallback to use a default reader");logging.warning(f" - file_reader.extension_mapping to map extensions")
39
+ return D
@@ -0,0 +1,58 @@
1
+ import logging,os,shutil,stat,subprocess,platform,tempfile,zipfile
2
+ from scilens.config.models import ExecuteConfig
3
+ from scilens.utils.file import dir_remove,dir_create
4
+ from scilens.utils.web import Web
5
+ def unzip_file(zip_file_path,extract_to_path):
6
+ with zipfile.ZipFile(zip_file_path,'r')as A:A.extractall(extract_to_path)
7
+ def find_command(command_path,working_dirs,guess_os_extension=False):
8
+ F='.bash';E='.sh';A=[]
9
+ if guess_os_extension:
10
+ B=platform.system().lower()
11
+ if B=='windows':A=['.exe','.bat','.cmd']
12
+ elif B=='linux':A=[E,F,'.bin']
13
+ elif B=='darwin':A=[E,F]
14
+ else:logging.warning(f"Unknown system {B}")
15
+ for G in working_dirs:
16
+ C=os.path.join(G,command_path)
17
+ if os.path.exists(C):return C
18
+ else:
19
+ for H in A:
20
+ D=C+H
21
+ if os.path.exists(D):return D
22
+ class Executor:
23
+ def __init__(A,absolute_working_dir,config,alternative_working_dir=None):
24
+ B=config;A.working_dir=absolute_working_dir;A.config=B;A.alternative_working_dir=alternative_working_dir;A.command_path=None;A.temp_dir=None
25
+ if B.exe_url and B.exe_path:raise ValueError('Executable URL and Path are defined. Only one can be defined.')
26
+ if not B.exe_url and not B.exe_path:raise ValueError('Executable URL and Path are not defined. One must be defined.')
27
+ if not os.path.exists(A.working_dir):logging.info(f"Creating working directory {A.working_dir}");dir_create(A.working_dir)
28
+ def __enter__(A):return A
29
+ def __exit__(A,exc_type,exc_value,traceback):A._cleanup()
30
+ def _cleanup(A):0
31
+ def _pre_operations(A):
32
+ logging.info(f"Execute - Pre Operations");logging.info(f"Folders deletion")
33
+ for dir in A.config.pre_folder_delete or[]:dir_remove(os.path.join(A.working_dir,dir))
34
+ logging.info(f"Folders creation")
35
+ for dir in A.config.pre_folder_creation or[]:dir_create(os.path.join(A.working_dir,dir))
36
+ if A.config.exe_url:logging.info(f"Download executable {A.config.exe_url}");A.temp_dir=tempfile.mkdtemp();E='executable';B=os.path.join(A.temp_dir,E);Web().download_progress(A.config.exe_url,B,headers=A.config.exe_url_headers,callback100=lambda percentage:logging.info(f"Downloaded {percentage}%"));logging.info(f"Download completed")
37
+ else:B=A.config.exe_path
38
+ if A.config.exe_unzip_and_use:logging.info(f"Unzip archive");C=os.path.dirname(B);unzip_file(B,C);B=os.path.join(C,A.config.exe_unzip_and_use);print(f"executable_path {B}");logging.info(f"Unzip completed")
39
+ if not os.path.exists(B):
40
+ if not A.config.exe_guess_os_extension:raise FileNotFoundError(f"Command not found: {B}")
41
+ else:
42
+ logging.info(f"Guess OS extension");D=[A.working_dir]
43
+ if A.alternative_working_dir:D.append(A.alternative_working_dir)
44
+ B=find_command(B,D,guess_os_extension=True)
45
+ if not B:raise FileNotFoundError(f"Command not found: {B}")
46
+ logging.info(f"Add executable permissions");F=os.stat(B).st_mode;os.chmod(B,F|stat.S_IXUSR|stat.S_IXGRP|stat.S_IXOTH);A.command_path=B
47
+ def _post_operations(A):logging.info(f"Execute - Post Operations")
48
+ def _run_command(A):
49
+ logging.info(f"Execute - Run Command");C=A.command_path;B=C
50
+ if os.path.isabs(B):
51
+ if not os.path.exists(B):raise FileNotFoundError(f"Command not found: {B}")
52
+ else:
53
+ D=[A.working_dir]
54
+ if A.alternative_working_dir:D.append(A.alternative_working_dir)
55
+ B=find_command(C,D,guess_os_extension=A.config.exe_guess_os_extension)
56
+ if not B:raise FileNotFoundError(f"Command not found: {C}")
57
+ E=f"{B}{A.config.command_suffix or''}";logging.info(f"RUN COMMAND {E} in {A.working_dir}");subprocess.run(E,shell=True,check=True,cwd=A.working_dir)
58
+ def process(A):logging.info(f"Execute");A._pre_operations();A._run_command();A._post_operations();A._cleanup()
@@ -0,0 +1,29 @@
1
+ _A=None
2
+ import logging,importlib,os
3
+ from scilens.config.models import FileReaderConfig
4
+ from scilens.config.models.readers import ReadersConfig
5
+ from scilens.readers.reader_manager import ReaderManager
6
+ from scilens.readers.exceptions import NoReaderFound
7
+ class FileReader:
8
+ def __init__(A,absolute_working_dir,config,readers_config,config_alternate_path=_A):A.path=absolute_working_dir;A.config_alternate_path=config_alternate_path;A.reader_mgmr=ReaderManager();A.config=config;A.readers_config=readers_config
9
+ def _get_custom_parser(D,config_parser):
10
+ E=config_parser
11
+ if not E:return
12
+ A,H=E.split('::');B=_A;I=[A,f"{D.path}/{A}",f"{D.config_alternate_path}/{A}"]
13
+ for C in I:
14
+ if os.path.exists(C):B=C;break
15
+ if not B:raise Exception(f"Custom curve parser not found: {A}")
16
+ J=C.split('/')[-1].replace('.py','');F=importlib.util.spec_from_file_location(J,B);G=importlib.util.module_from_spec(F);F.loader.exec_module(G);return getattr(G,H)
17
+ def read(A,path):
18
+ logging.info(f"Reading file: {path}");E=_A;C=A.config.custom_curve_parser
19
+ if C:
20
+ if isinstance(C,str):E=A._get_custom_parser(C)
21
+ try:
22
+ B=A.reader_mgmr.get_reader_from_file(path,encoding=A.config.encoding,curve_parser=E,extension_mapping=A.config.extension_mapping,extension_fallback=A.config.extension_fallback);D=_A
23
+ if B.__class__.__name__=='ReaderTxt':D=A.readers_config.txt
24
+ elif B.__class__.__name__=='ReaderCsv':D=A.readers_config.csv
25
+ B.read(D)
26
+ except NoReaderFound:
27
+ if A.config.extension_unknown_ignore:0
28
+ else:raise Exception(f"No reader found")
29
+ return B
File without changes
@@ -0,0 +1,8 @@
1
+ _A=False
2
+ def get_vars(title='',export_html=_A,export_json=_A,export_yaml=_A):
3
+ C=title;B=True;A={}
4
+ if C:A['report.title']=C
5
+ if export_html:A['report.output.export_html']=B
6
+ if export_json:A['report.output.export_json']=B
7
+ if export_yaml:A['report.output.export_yaml']=B
8
+ return A
@@ -0,0 +1,3 @@
1
+ from scilens.app import pkg_name
2
+ ENV_VARS_CONFIG_MODEL=['processor','execute.command_suffix','execute_and_compare.test.exe_path','execute_and_compare.reference.exe_path','report.title']
3
+ def get_vars():return{f"{pkg_name.upper()}_{A.upper().replace('.','_')}":A for A in ENV_VARS_CONFIG_MODEL}
scilens/config/load.py ADDED
@@ -0,0 +1,40 @@
1
+ import logging,os,json,yaml
2
+ from pydantic import BaseModel
3
+ from scilens.config.models import AppConfig
4
+ from scilens.config.env_var import get_vars
5
+ from scilens.utils.dict import dict_path_set,dict_path_get
6
+ PATH_ENV_VARS_VALUES={}
7
+ def env_vars_load():
8
+ B=get_vars()
9
+ for(C,D)in B.items():
10
+ A=os.getenv(C)
11
+ if A:PATH_ENV_VARS_VALUES[D]=A
12
+ env_vars_load()
13
+ def config_load(config,options_path_value=None,config_override=None):
14
+ F=options_path_value;B=config_override;A=config;J=isinstance(A,str)or isinstance(B,str);K=bool(PATH_ENV_VARS_VALUES);L=bool(F)
15
+ if isinstance(A,str):
16
+ logging.info(f"Loading configuration file {A}")
17
+ with open(A,'r')as G:
18
+ try:C=yaml.safe_load(G)
19
+ except yaml.YAMLError as D:raise Exception(f"Error in configuration file {A}: {D}")
20
+ elif isinstance(A,dict):C=json.loads(json.dumps(A))
21
+ if B:
22
+ if isinstance(B,str):
23
+ logging.info(f"Loading configuration file {B}")
24
+ with open(B,'r')as G:
25
+ try:I=yaml.safe_load(G)
26
+ except yaml.YAMLError as D:raise Exception(f"Error in configuration file {B}: {D}")
27
+ elif isinstance(A,dict):I=B
28
+ C.update(I)
29
+ for(E,H)in PATH_ENV_VARS_VALUES.items():
30
+ if not dict_path_get(C,E):dict_path_set(C,E,H)
31
+ if F:
32
+ for(E,H)in F.items():dict_path_set(C,E,H)
33
+ try:return AppConfig(**C)
34
+ except Exception as D:
35
+ logging.error(f"Error in configuration definition")
36
+ if J:logging.error(f"Please check Configuration file: {A}")
37
+ if K:logging.error(f"Please check Environment Variables")
38
+ if L:logging.error(f"Please check Command Options")
39
+ logging.warning(f"{D}");exit(1)
40
+ def pydantic_to_yaml(model):return yaml.safe_dump(model.model_dump(by_alias=True),default_flow_style=False)
@@ -0,0 +1,12 @@
1
+ from.app import AppConfig
2
+ from.compare import CompareConfig
3
+ from.compare_float_thresholds import CompareFloatThresholdsConfig
4
+ from.execute_and_compare import ExecuteAndCompareConfig
5
+ from.execute import ExecuteConfig
6
+ from.reader_format_txt import ReaderTxtIgnoreConfig,ReaderTxtConfig
7
+ from.reader_format_csv import ReaderCsvConfig
8
+ from.readers import ReadersConfig
9
+ from.file_reader import FileReaderConfig
10
+ from.report_html import ReportHtmlConfig,ReportHtmlCurvesConfig,ReportParameterOpenFileInConfig,ReportParameterPageModeConfig
11
+ from.report_output import ReportOutputConfig
12
+ from.report import ReportConfig
@@ -0,0 +1,9 @@
1
+ _A='Configuration utile au processeur `ExecuteAndCompare`.'
2
+ from pydantic import BaseModel,Field
3
+ from scilens.config.models.compare import CompareConfig
4
+ from scilens.config.models.execute import ExecuteConfig
5
+ from scilens.config.models.execute_and_compare import ExecuteAndCompareConfig
6
+ from scilens.config.models.file_reader import FileReaderConfig
7
+ from scilens.config.models.readers import ReadersConfig
8
+ from scilens.config.models.report import ReportConfig
9
+ class AppConfig(BaseModel):processor:str=Field(description='Nom du processeur à utiliser. `Compare` ou `ExecuteAndCompare`.');variables:dict[str,str]=Field(default={},description='Variables Utilisateur.');tags:list[str]|None=Field(default=None,description="Utilisé dans un contexte ligne de commande, utilisé en conjonction avec l'option `--discover` pour filtrer les cas à éxécuter.");execute:ExecuteConfig=Field(default=ExecuteConfig(),description=_A);execute_and_compare:ExecuteAndCompareConfig=Field(default=ExecuteAndCompareConfig(),description=_A);file_reader:FileReaderConfig=Field(default=FileReaderConfig(),description='Configuration des readers fichiers.');readers:ReadersConfig=Field(default=ReadersConfig(),description='Configuration des readers.');compare:CompareConfig=Field(default=CompareConfig(),description='Configuration utile aux processeurs `Compare` et `ExecuteAndCompare`');report:ReportConfig=Field(default=ReportConfig(),description='Configuration des Reports.')
@@ -0,0 +1,5 @@
1
+ _A=None
2
+ from pydantic import BaseModel,Field
3
+ from scilens.config.models.compare_float_thresholds import CompareFloatThresholdsConfig
4
+ class CompareSourceFoldersConfig(BaseModel):test_folder_relative_path:str=Field(default='test',description='Relative path to the working directory for the test folder.');reference_folder_relative_path:str=Field(default='reference',description='Relative path to the working directory for the reference folder.');additional_path_suffix:str=Field(default='',description='Additional path to add after test and reference folders.');test_filename_match_ignore:str|_A=Field(default=_A,description='Chaîne spécifique dans le nom de fichier test à ignorer pour matcher les noms. Ex : `.test` or `test_`');reference_filename_match_ignore:str|_A=Field(default=_A,description='Chaîne spécifique dans le nom de fichier référence à ignorer pour matcher les noms. Ex : `.ref` or `ref_`');recursive:bool=Field(default=False,description='Recherche récursive dans les répertoires de test et de référence.')
5
+ class CompareConfig(BaseModel):sources:CompareSourceFoldersConfig=Field(default=CompareSourceFoldersConfig(),description='Configuration pour les sources des données de test et de référence.');float_thresholds:CompareFloatThresholdsConfig=Field(default=CompareFloatThresholdsConfig(),description='Seuil de comparaison pour les valeurs flottantes.');ignore_warnings:bool=Field(default=False,description='Si `true`, ne lève pas les erreurs de sévérité `warning`.');errors_limit:int=Field(default=1000,description="Limite d'erreurs à atteindre avant de s'arrêter (Sévérité `warning` et `error`).")
@@ -0,0 +1,4 @@
1
+ from pydantic import BaseModel,Field
2
+ class CompareFloatVectorsAmplitudeConfig(BaseModel):method:str=Field(default='soften',description="Si `soften`, réduit la sévérité de l'erreur. Si `ignore`, ne lève pas d'erreur.");multiplier:float=Field(description='Processe la méthode si `|Test-Reference|` (Erreur Absolue) est inférieure à `multiplier * |Max(Vector)-Min(Vector)|` (Amplitude pondérée). Ex: `0.1`.')
3
+ class CompareFloatVectorsConfig(BaseModel):amplitude_moderation:CompareFloatVectorsAmplitudeConfig|None=Field(default=None,description="Pondére la gestion d'erreur avec l'amplitude.")
4
+ class CompareFloatThresholdsConfig(BaseModel):relative_vs_absolute_min:float=Field(default=1e-12,description="Si la valeur de test est inférieure à ce seuil, calcul de l'erreur absolue.");relative_error_min:float=Field(default=.001,description="Si l'erreur relative est supérieure à ce seuil, génère une erreur de sévérité `warning`.");relative_error_max:float=Field(default=.01,description="Si l'erreur relative est supérieure à ce seuil, génère une erreur de sévérité `error`.");absolute_error_min:float=Field(default=1e-07,description="Si l'erreur absolue est supérieure à ce seuil, génère une erreur de sévérité `warning`.");absolute_error_max:float=Field(default=1e-06,description="Si l'erreur absolue est supérieure à ce seuil, génère une erreur de sévérité `error`.");vectors:CompareFloatVectorsConfig=Field(default=CompareFloatVectorsConfig(),description='Paramètres pour la comparaison de vecteurs de flottants (csv, nc, ...).')
@@ -0,0 +1,4 @@
1
+ _B="Url de l'exécutable à télécharger avant de l'exécuter. Ex : `https://github.com/org/app/releases/download/v3.31.5/app-2.12.2-linux-x86-64`."
2
+ _A=None
3
+ from pydantic import BaseModel,Field
4
+ class ExecuteConfig(BaseModel):pre_folder_delete:list[str]|_A=Field(default=_A,description='Liste de répertoire à supprimer avant l\'éxecution. Ex: `["DEBUG", "LOG", "OUT"]`');pre_folder_creation:list[str]|_A=Field(default=_A,description='Liste de répertoire à créeravant l\'éxecution. Ex : `["INI", "DAT"]`');exe_path:str|_A=Field(default=_A,description="Chemin de l'exécutable. Ex : `/absolute/path/executable` or `relative/path/executable`. Dans le cas, d'un chemin relatif, il testera le `working directory` et le `origin working directory`.");exe_url:str|_A=Field(default=_A,description=_B);exe_url_headers:dict[str,str]|_A=Field(default=_A,description=_B);exe_unzip_and_use:str|_A=Field(default=_A,description="Si l'exécutable (path ou url) est zippé, alors décompresse et utilise le fichier spécifié. Ex : `app-2.12.2-linux-x86-64`");exe_guess_os_extension:bool=Field(default=False,description="Si la commande à exécuter n'est pas trouvée, cherche avec des extensions en fonction de l'OS. Ex : `.exe`, `.bat` pour windows");command_suffix:str|_A=Field(default=_A,description='Suffixe à ajouter à la commande. Typiquemnt des secrets. Ex : ` --token DGDFGDFGDH`');working_dir:str|_A=Field(default=_A,description="Chemin relatif du répertoire de travail pour l'éxecution de la commande. Dans le contexte processeur `ExecuteAndCompare` si non défini, récupère respectivement la valeur de `compare.sources.test_folder_relative_path` ou `compare.sources.reference_folder_relative_path`")
@@ -0,0 +1,4 @@
1
+ _A=None
2
+ from pydantic import BaseModel,Field
3
+ from scilens.config.models.execute import ExecuteConfig
4
+ class ExecuteAndCompareConfig(BaseModel):test:ExecuteConfig|_A=Field(default=_A,description='Surcharge les paramètres de la section `execute` pour le contexte de test.');test_only:bool=Field(default=False,description='Si `true`, aucune éxécution faite pour la référence (les sorties de réferences pour comparaison existent déjà).');reference:ExecuteConfig|_A=Field(default=_A,description='Surcharge les paramètres de la section `execute` pour le contexte de référence.')
@@ -0,0 +1,3 @@
1
+ _A=None
2
+ from pydantic import BaseModel,Field
3
+ class FileReaderConfig(BaseModel):extension_unknown_ignore:bool=Field(default=False,description="Si aucun reader n'est trouvé pour une extension, passe au suivant sans générer d'erreur.");extension_mapping:dict|_A=Field(default=_A,description='Mapping d\'extensions source - cible. Ex `{"txt": "csv"}`.');extension_fallback:str|_A=Field(default=_A,description="Si aucun reader n'est trouvé pour une extension, utilise cette extension pour déterminer le reader.");encoding:str=Field(default='utf-8',description='Encodage utilisé pour lire les fichiers.');custom_curve_parser:str|dict[str,str]|_A=Field(default=_A,description="Représente le chemin d'un fichier/module python suivi par :: suivi par le nom d'une fonction à appeller pour parser les courbes d'un fichier. Le chemin peut être absolu ou relatif. Dans le cas, d'un chemin relatif, il testera le `working directory` et le `origin working directory`. Ex: `custom_parser.py::parse_curves`")
@@ -0,0 +1,18 @@
1
+ _B='(Valide seulement si la première ligne est les en-têtes)'
2
+ _A=None
3
+ from pydantic import BaseModel,Field,model_validator
4
+ from enum import Enum
5
+ class ReaderCsvCurveParserNameConfig(str,Enum):COL_X='col_x';COLS_COUPLE='cols_couple'
6
+ class ReaderCsvCurveParserColXConfig(BaseModel):x:int|str|list[str]=Field(default=1,description="Si `integer`, index de la colonne pour l'axe x (index `1` pour la première colonne)."+"Si `String`, nom de la colonne pour l'axe x. Si la colonne n'existe pas, il n'y aura pas de parsing."+_B+"Si `List[String] (utiles dans un contexte multi dataset), noms de la colonne pour l'axe x, noms des colonnes pour l'axe x , si plusieurs colonnes correspondent, la première trouvée sera utilisée. Si aucune colonne ne correspond, il n'y aura pas de parsing."+_B)
7
+ class ReaderCsvCurveParserColsCoupleConfig(BaseModel):x_index:int=Field(description='NOT IMPLEMENTED - Index Col x');y_index:int=Field(description='NOT IMPLEMENTED - Index Col y')
8
+ class ReaderCsvCurveParserConfig(BaseModel):
9
+ name:ReaderCsvCurveParserNameConfig=Field(description='Le type de `paramètres` dépend de cette valeur');parameters:ReaderCsvCurveParserColXConfig|ReaderCsvCurveParserColsCoupleConfig=Field(description='Paramètres du parser de courbes')
10
+ @model_validator(mode='after')
11
+ def validate_model(cls,model):
12
+ A=model
13
+ if isinstance(A.parameters,ReaderCsvCurveParserColXConfig)and A.name!=ReaderCsvCurveParserNameConfig.COL_X or isinstance(A.parameters,ReaderCsvCurveParserColsCoupleConfig)and A.name!=ReaderCsvCurveParserNameConfig.COLS_COUPLE:raise ValueError(f"Curve Parser {A.name} Parameters are not correct")
14
+ if isinstance(A.parameters,dict):
15
+ if A.name==ReaderCsvCurveParserNameConfig.COL_X:A.parameters=ReaderCsvCurveParserColXConfig(**A.parameters)
16
+ elif A.name==ReaderCsvCurveParserNameConfig.COLS_COUPLE:A.parameters=ReaderCsvCurveParserColsCoupleConfig(**A.parameters)
17
+ return A
18
+ class ReaderCsvConfig(BaseModel):ignore_colunmns:list[str]|_A=Field(default=_A,description='Liste des noms des colonnes à ignorer. (Valide seulement si la première ligne est les en-têtes)');curve_parser:ReaderCsvCurveParserConfig|_A=Field(default=_A,description='Parseur de courbe à utiliser.')
@@ -0,0 +1,5 @@
1
+ _A=None
2
+ from pydantic import BaseModel,Field
3
+ class ReaderTxtReportLines(BaseModel):pre:int=Field(default=0,description="Nombre de ligne avant l'erreur à présenter avec le message d'erreur.");post:int=Field(default=0,description="Nombre de ligne après l'erreur à présenter avec le message d'erreur.")
4
+ class ReaderTxtIgnoreConfig(BaseModel):pattern:str=Field(default='',description='Motif de chaîne à ignorer.');pre:int=Field(default=0,description='Nombre de ligne supplémentaires à ignorer avant.');post:int=Field(default=0,description='Nombre de ligne supplémentaires à ignorer après.')
5
+ class ReaderTxtConfig(BaseModel):error_rule_patterns:list[str]|_A=Field(default=_A,description='Liste de motifs de chaîne à vérifier. Si un motif est trouvé, génère une erreur. Ex: `["NaN", "infinity"]`');ignore:dict[str,list[ReaderTxtIgnoreConfig]]|_A=Field(default=_A,description='Ignore les lignes d\'un fichier selon un motif. Ex: `{"path": [{"pattern": "NaN", "pre": 0, "post": 0}]}`');report_lines:dict[str,ReaderTxtReportLines]|_A=Field(default=_A,description='Nombre de lignes à reporter avant et après une erreur. Ex: `{"path": {"pre": 0, "post": 0}`')
@@ -0,0 +1,4 @@
1
+ from pydantic import BaseModel,Field
2
+ from scilens.config.models.reader_format_txt import ReaderTxtIgnoreConfig,ReaderTxtConfig
3
+ from scilens.config.models.reader_format_csv import ReaderCsvConfig
4
+ class ReadersConfig(BaseModel):txt:ReaderTxtConfig=Field(default=ReaderTxtConfig(),description='Configuration des readers txt.');csv:ReaderCsvConfig=Field(default=ReaderCsvConfig(),description='Configuration des readers csv.')
@@ -0,0 +1,6 @@
1
+ _A=None
2
+ from pydantic import BaseModel,Field
3
+ from scilens.app import product_name
4
+ from scilens.config.models.report_html import ReportHtmlConfig
5
+ from scilens.config.models.report_output import ReportOutputConfig
6
+ class ReportConfig(BaseModel):debug:bool=Field(default=False,description='If `true` the report will be more verbose and displays template input data.');title:str=Field(default=_A,description='Titre du rapport');title_prefix:str=Field(default=f"{product_name} Report",description='Préfixe au Titre du rapport');logo:str|_A=Field(default=_A,description="Source d'une image logo. Peut être une url ou une url data image encodée base64 (exclusif avec `logo_file`).");logo_file:str|_A=Field(default=_A,description="Source fichier d'une image logo (exclusif avec `logo`).");output:ReportOutputConfig=ReportOutputConfig();html:ReportHtmlConfig=ReportHtmlConfig()
@@ -0,0 +1,6 @@
1
+ _A="Paramètre modifiable par l'utilisateur"
2
+ from pydantic import BaseModel,Field
3
+ class ReportParameterPageModeConfig(BaseModel):is_user_preference:bool=Field(default=True,description=_A);default_value:str=Field(default='onepage',description='`tabs` ou `onepage`')
4
+ class ReportParameterOpenFileInConfig(BaseModel):is_user_preference:bool=Field(default=True,description=_A);default_value:str=Field(default='browser',description='`browser` ou `vscode`')
5
+ class ReportHtmlCurvesConfig(BaseModel):display_on_load:bool=Field(default=False,description="Si `true`, affiche tous les graphiques courbes à l'ouverture du rapport.");init_width:int=Field(default=600,description='Largeur initiale des graphiques courbes.');init_height:int=Field(default=400,description='Hauteur initiale des graphiques courbes.')
6
+ class ReportHtmlConfig(BaseModel):compare_color_test:str=Field(default='1982c4',description='Couleur pour les données de test.');compare_color_reference:str=Field(default='6a4c93',description='Couleur pour les données de référence.');collapse_if_successful:bool=Field(default=True,description="N'affiche par défaut les sections de comparaison sans erreur.");parameter_page_mode:ReportParameterPageModeConfig=ReportParameterPageModeConfig();parameter_open_file_in:ReportParameterOpenFileInConfig=ReportParameterOpenFileInConfig();curves:ReportHtmlCurvesConfig=ReportHtmlCurvesConfig()
@@ -0,0 +1,4 @@
1
+ _A=False
2
+ from pydantic import BaseModel,Field
3
+ from scilens.app import pkg_name
4
+ class ReportOutputConfig(BaseModel):filename:str=Field(default=f"{pkg_name}_report",description="Nom de fichier (sans l'extension) des rapports générés.");export_html:bool=Field(default=_A,description='Génère un rapport HTML');export_txt:bool=Field(default=_A,description='Génère un rapport TXT');export_json:bool=Field(default=_A,description='Génère un rapport JSON');export_yaml:bool=Field(default=_A,description='Génère un rapport YAML');export_py:bool=Field(default=_A,description='Génère un rapport Python (Variable)');export_js:bool=Field(default=_A,description='Génère un rapport Javascript (Variable)');export_ts:bool=Field(default=_A,description='Génère un rapport Typecript (Variable)');export_php:bool=Field(default=_A,description='Génère un rapport Php (Variable)')
@@ -0,0 +1,27 @@
1
+ _A=None
2
+ import logging,os
3
+ from scilens.app import pkg_name
4
+ from scilens.utils.file import dir_is_empty,dir_remove,dir_create,copy
5
+ from scilens.helpers.search_and_index import SearchAndIndex,PathSearchInfo
6
+ class Assets:
7
+ def __init__(A,path,force_clean=False,force_create=False):
8
+ C=path;A.path=C if os.path.isabs(C)else os.path.abspath(C);logging.info(f"Assets directory : {A.path}")
9
+ if os.path.exists(A.path):
10
+ logging.info(f"Assets directory exists")
11
+ if force_clean:logging.info(f"Cleaning assets directory {A.path}");dir_remove(A.path);dir_create(A.path)
12
+ elif not dir_is_empty(A.path):B='Assets directory is not empty';logging.error(B);raise Exception(B)
13
+ else:logging.info(f"Assets directory is empty")
14
+ else:
15
+ logging.info(f"Assets directory does not exists")
16
+ if force_create:logging.info(f"Creating assets directory");dir_create(A.path)
17
+ else:B='Assets directory does not exist';logging.error(B);raise Exception(B)
18
+ def copy(E,paths_infos,items_only=_A):
19
+ B=items_only
20
+ for A in paths_infos:
21
+ C=os.path.join(E.path,A.relative_dir)
22
+ if B:
23
+ for D in B:F=os.path.join(A.dir,D);G=os.path.join(C,D);copy(F,G)
24
+ else:copy(A.dir,C)
25
+ def create_html_index(A,index_path,search_path,search_filename,title=_A,logo=_A,logo_file=_A):SearchAndIndex().search_and_create_html_index(search_path,search_filename,index_path,title=title,logo=logo,logo_file=logo_file)
26
+ def report_get_name(A):return f"{pkg_name}_report.html"
27
+ def report_discover(A,discover_path):return SearchAndIndex().file_discover(discover_path,A.report_get_name())