@@ -64,3 +64,154 @@ def download_archive(
6464 output_dir = output ,
6565 )
6666 typer .echo (f"Download complete → { output } " )
67+
68+
69+ def _download_files (
70+ archive_id : int ,
71+ token : str | None ,
72+ date : str | None ,
73+ start : str | None ,
74+ end : str | None ,
75+ ) -> list :
76+ """Download archive files (shared by sheets and exec commands)."""
77+ from esios .cli .app import get_client
78+
79+ if not date and not (start and end ):
80+ typer .echo ("Provide --date or both --start and --end." , err = True )
81+ raise typer .Exit (1 )
82+
83+ client = get_client (token )
84+
85+ if date :
86+ files = client .archives .download (archive_id , date = date )
87+ else :
88+ files = client .archives .download (archive_id , start = start , end = end )
89+
90+ if not files :
91+ typer .echo ("No files found for the given date range." , err = True )
92+ raise typer .Exit (1 )
93+
94+ return files
95+
96+
97+ @archives_app .command ("sheets" )
98+ def sheets (
99+ archive_id : int = typer .Argument (..., help = "Archive ID (e.g. 34 for I90DIA)" ),
100+ date : Optional [str ] = typer .Option (None , "--date" , "-d" , help = "Single date (YYYY-MM-DD)" ),
101+ start : Optional [str ] = typer .Option (None , "--start" , "-s" , help = "Start date (YYYY-MM-DD)" ),
102+ end : Optional [str ] = typer .Option (None , "--end" , "-e" , help = "End date (YYYY-MM-DD)" ),
103+ token : Optional [str ] = typer .Option (None , "--token" , "-t" ),
104+ ):
105+ """List sheets (table of contents) in an archive file.
106+
107+ Downloads and parses the first file to show available sheets.
108+
109+ \b
110+ Examples:
111+ esios archives sheets 34 --date 2025-06-01
112+ esios archives sheets 34 --start 2025-06-01 --end 2025-06-01
113+ """
114+ from esios .processing .i90 import I90Book
115+
116+ files = _download_files (archive_id , token , date , start , end )
117+
118+ try :
119+ book = I90Book (files [0 ])
120+ except Exception as exc :
121+ typer .echo (f"Error parsing { files [0 ].name } : { exc } " , err = True )
122+ raise typer .Exit (1 )
123+
124+ table = Table (title = f"Sheets in { files [0 ].name } " )
125+ table .add_column ("Sheet" , style = "cyan" )
126+ table .add_column ("Description" )
127+
128+ for sheet_name , description in book .table_of_contents .items ():
129+ if not isinstance (sheet_name , str ) or not sheet_name .strip ():
130+ continue
131+ table .add_row (str (sheet_name ), str (description ))
132+
133+ console .print (table )
134+
135+
136+ @archives_app .command ("exec" )
137+ def exec_archive (
138+ archive_id : int = typer .Argument (..., help = "Archive ID (e.g. 34 for I90DIA)" ),
139+ sheet : str = typer .Option (..., "--sheet" , help = "Sheet name (e.g. I90DIA03, I90DIA26)" ),
140+ date : Optional [str ] = typer .Option (None , "--date" , "-d" , help = "Single date (YYYY-MM-DD)" ),
141+ start : Optional [str ] = typer .Option (None , "--start" , "-s" , help = "Start date (YYYY-MM-DD)" ),
142+ end : Optional [str ] = typer .Option (None , "--end" , "-e" , help = "End date (YYYY-MM-DD)" ),
143+ expr : str = typer .Option ("df" , "--expr" , "-x" , help = "Python expression to evaluate (df, pd, np available)" ),
144+ format : str = typer .Option ("table" , "--format" , "-f" , help = "Output format: table, csv, json" ),
145+ output : Optional [str ] = typer .Option (None , "--output" , "-o" , help = "Output file path" ),
146+ token : Optional [str ] = typer .Option (None , "--token" , "-t" ),
147+ ):
148+ """Parse archive files and evaluate a Python expression on the data.
149+
150+ Downloads archive files (cached), parses the specified sheet from each
151+ file using I90Book, concatenates the DataFrames, and evaluates the
152+ expression. The parsed data is available as `df` (pandas DataFrame).
153+ `pd` (pandas) and `np` (numpy) are also available.
154+
155+ \b
156+ Examples:
157+ # Show curtailment data for a single day
158+ esios archives exec 34 --sheet I90DIA03 --date 2025-06-01
159+
160+ # Curtailment by technology over a month
161+ esios archives exec 34 --sheet I90DIA03 -s 2025-05-05 -e 2025-06-08 \\
162+ -x "df[df['Sentido']=='Bajar'].groupby('Tecnología')['value'].sum().sort_values()"
163+
164+ # Export PBF generation program to CSV
165+ esios archives exec 34 --sheet I90DIA26 --date 2025-06-01 -f csv -o pbf.csv
166+
167+ # Descriptive statistics
168+ esios archives exec 34 --sheet I90DIA03 --date 2025-06-01 -x "df.describe()"
169+ """
170+ import numpy as np
171+ import pandas as pd
172+
173+ from esios .processing .i90 import I90Book
174+
175+ files = _download_files (archive_id , token , date , start , end )
176+
177+ # Parse all files and extract the requested sheet
178+ all_dfs = []
179+ for f in files :
180+ try :
181+ book = I90Book (f )
182+ s = book [sheet ]
183+ if s .df is not None and not s .df .empty :
184+ all_dfs .append (s .df .reset_index ())
185+ except KeyError :
186+ # Sheet not found — show available sheets
187+ try :
188+ book = I90Book (f )
189+ available = list (book .table_of_contents .keys ())
190+ except Exception :
191+ available = []
192+ typer .echo (f"Sheet '{ sheet } ' not found in { f .name } ." , err = True )
193+ if available :
194+ typer .echo (f"Available sheets: { ', ' .join (str (s ) for s in available )} " , err = True )
195+ raise typer .Exit (1 )
196+ except Exception as exc :
197+ typer .echo (f"Warning: skipping { f .name } : { exc } " , err = True )
198+ continue
199+
200+ if not all_dfs :
201+ typer .echo ("No data extracted from the specified sheet." , err = True )
202+ raise typer .Exit (1 )
203+
204+ df = pd .concat (all_dfs , ignore_index = True )
205+
206+ # Evaluate expression
207+ namespace = {"df" : df , "pd" : pd , "np" : np }
208+ try :
209+ result = eval (expr , {"__builtins__" : {}}, namespace ) # noqa: S307
210+ except Exception as exc :
211+ typer .echo (f"Error evaluating expression: { exc } " , err = True )
212+ raise typer .Exit (1 )
213+
214+ # Render output — reuse _render from exec_cmd
215+ from esios .cli .exec_cmd import _render
216+
217+ _render (result , format , output )
0 commit comments