11"""Download EIA-930 data."""
22
3+ import re
34from pathlib import Path
5+ from urllib .parse import urljoin
46
57import pandas as pd
8+ from playwright .async_api import async_playwright , expect
69
710from pudl_archiver .archivers .classes import (
811 AbstractDatasetArchiver ,
1619REFERENCE_URL = (
1720 "https://www.eia.gov/electricity/930-content/EIA930_Reference_Tables.xlsx"
1821)
22+ ABOUT_URL = "https://www.eia.gov/electricity/gridmonitor/about"
1923
2024
2125class Eia930Archiver (AbstractDatasetArchiver ):
2226 """EIA 930 archiver."""
2327
2428 name = "eia930"
2529
26- async def get_file_list (self ) -> pd .DataFrame :
30+ async def get_eia930_file_list (self ) -> pd .DataFrame :
2731 """Get EIA 930 file list dataframe."""
2832 return pd .read_csv (FILE_LIST_URL )
2933
@@ -36,28 +40,66 @@ async def get_reference_table(self) -> pd.DataFrame:
3640 partitions = {"half_year" : "all" , "form" : "reference" },
3741 )
3842
43+ async def after_download (self ) -> None :
44+ """Clean up playwright once downloads are complete."""
45+ await self .browser .close ()
46+ await self .playwright .stop ()
47+
48+ async def get_eia930a_files (self ) -> dict [str , str ]:
49+ """Get a dictionary of EIA 930A file download URLs indexed by year."""
50+ link_dict = {}
51+ self .playwright = await async_playwright ().start ()
52+ self .browser = await self .playwright .webkit .launch ()
53+
54+ link_pattern = re .compile (r"EIA_930A_(\d{4})_with layout.xlsx" )
55+ # Get main table links using playwright.
56+ page = await self .browser .new_page ()
57+ await page .goto (ABOUT_URL , timeout = 10 * 60 * 1000 )
58+ await expect (
59+ page .get_by_text ("About the EIA-930 data" )
60+ ).to_be_visible () # Wait for reference URL to load before proceeding.
61+ text = await page .content ()
62+ links = self .get_hyperlinks_from_text (text , link_pattern , ABOUT_URL )
63+
64+ for link in links :
65+ matches = link_pattern .search (link )
66+ if not matches :
67+ continue
68+ year = int (matches .group (1 ))
69+ link_dict .update ({year : link })
70+ return link_dict
71+
3972 async def get_resources (self ) -> ArchiveAwaitable :
4073 """Download EIA-930 resources."""
41- file_list = await self .get_file_list ()
74+ eia930_file_list = await self .get_eia930_file_list ()
4275 year_period = (
43- file_list [["YEAR" , "PERIOD" ]]
76+ eia930_file_list [["YEAR" , "PERIOD" ]]
4477 .value_counts ()
4578 .reset_index ()
4679 .drop (columns = ["count" ])
4780 .sort_values ("YEAR" )
4881 )
4982 for index , period in year_period .iterrows ():
5083 if self .valid_year (period .YEAR ):
51- yield self .get_year_resource (
52- file_list = file_list , year = period .YEAR , half_year = period .PERIOD
84+ yield self .get_eia930_half_year_resource (
85+ file_list = eia930_file_list ,
86+ year = period .YEAR ,
87+ half_year = period .PERIOD ,
88+ )
89+
90+ eia930a_file_list = await self .get_eia930a_files ()
91+ for year in eia930a_file_list :
92+ if self .valid_year (year ):
93+ yield self .get_eia930a_year_resource (
94+ file = eia930a_file_list [year ], year = year
5395 )
5496 yield self .get_reference_table ()
5597
56- async def get_year_resource (
98+ async def get_eia930_half_year_resource (
5799 self , file_list : pd .DataFrame , year = int , half_year = int
58100 ) -> tuple [Path , dict ]:
59- """Download zip file of all files in year."""
60- self .logger .debug (f"Downloading data for { year } half{ half_year } ." )
101+ """Download zip file of all files in a half- year."""
102+ self .logger .debug (f"Downloading EIA 930 data for { year } half{ half_year } ." )
61103 zip_path = self .download_directory / f"eia930-{ year } half{ half_year } .zip"
62104 data_paths_in_archive = set ()
63105 period_files = file_list [
@@ -80,6 +122,28 @@ async def get_year_resource(
80122
81123 return ResourceInfo (
82124 local_path = zip_path ,
83- partitions = {"half_year" : f"{ year } half{ half_year } " },
125+ partitions = {"half_year" : f"{ year } half{ half_year } " , "form" : "eia930" },
126+ layout = ZipLayout (file_paths = data_paths_in_archive ),
127+ )
128+
129+ async def get_eia930a_year_resource (
130+ self , file : str , year = int , half_year = int
131+ ) -> tuple [Path , dict ]:
132+ """Download zip file of all files in year for EIA 930-A."""
133+ self .logger .debug (f"Downloading EIA930A data for { year } ." )
134+ zip_path = self .download_directory / f"eia930a-{ year } .zip"
135+ data_paths_in_archive = set ()
136+
137+ url = urljoin (ABOUT_URL , file )
138+ file_type = url .split ("." )[
139+ - 1
140+ ] # Infer filetype based on url, rather than assigning
141+ filename = f"eia930a-{ year } .{ file_type } "
142+ await self .download_and_zip_file (url , filename , zip_path )
143+ data_paths_in_archive .add (filename )
144+
145+ return ResourceInfo (
146+ local_path = zip_path ,
147+ partitions = {"year" : year , "form" : "eia930a" },
84148 layout = ZipLayout (file_paths = data_paths_in_archive ),
85149 )
0 commit comments