Skip to content

Commit ba6f7ae

Browse files
authored
Merge pull request #1283 from catalyst-cooperative/archive-eia930a
Ella is off for the next week and I don't have any blocking concerns here so I'm going to YOLO it and merge.
2 parents 90ad1ea + 2fd10b0 commit ba6f7ae

1 file changed

Lines changed: 73 additions & 9 deletions

File tree

src/pudl_archiver/archivers/eia/eia930.py

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Download EIA-930 data."""
22

3+
import re
34
from pathlib import Path
5+
from urllib.parse import urljoin
46

57
import pandas as pd
8+
from playwright.async_api import async_playwright, expect
69

710
from pudl_archiver.archivers.classes import (
811
AbstractDatasetArchiver,
@@ -16,14 +19,15 @@
1619
REFERENCE_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

2125
class 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

Comments
 (0)