Skip to content

Commit 07b4495

Browse files
committed
document rateacuity api
1 parent b4cc135 commit 07b4495

File tree

1 file changed

+48
-0
lines changed

1 file changed

+48
-0
lines changed

tariff_fetch/rateacuity/state.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
"""State-machine style helpers for navigating the RateAcuity portal.
2+
3+
The module exposes a small set of classes that wrap Selenium interactions and
4+
provide a linear flow for logging in, picking reporting parameters, and
5+
downloading prepared benchmark reports as Polars dataframes.
6+
7+
Usage
8+
-----
9+
from tariff_fetch.rateacuity.base import create_context
10+
from tariff_fetch.rateacuity.state import LoginState
11+
12+
with create_context() as context:
13+
report = (
14+
LoginState(context)
15+
.login(username, password)
16+
.electric()
17+
.benchmark()
18+
.select_state("NY")
19+
.select_utility("Consolidated Edison Company of New York")
20+
.select_schedule("Residential Service")
21+
)
22+
df = report.as_dataframe()
23+
24+
The resulting ``df`` contains the cleaned benchmark data for the selected
25+
schedule.
26+
"""
27+
128
from __future__ import annotations
229

330
import argparse
@@ -19,39 +46,49 @@
1946

2047

2148
class State:
49+
"""Shared Selenium helpers as the base for the strongly-typed state objects."""
50+
2251
def __init__(self, context: ScrapingContext):
52+
"""Store the scraping context that contains the shared webdriver."""
2353
self._context = context
2454

2555
@property
2656
def driver(self) -> Chrome:
57+
"""Expose the underlying Selenium driver used for navigation."""
2758
return self._context.driver
2859

2960
def _logged_in(self) -> bool:
61+
"""Return True if the login link is absent, indicating an authenticated session."""
3062
return not self.driver.find_elements(By.ID, "loginLink")
3163

3264
def _wait(self) -> WebDriverWait:
65+
"""Create a wait helper bound to the current driver instance."""
3366
return WebDriverWait(self.driver, 10)
3467

3568

3669
class LoginState(State):
3770
def login(self, username: str, password: str) -> PortalState:
71+
"""Authenticate with RateAcuity and transition into the portal state."""
3872
login(self._context.driver, username, password)
3973
return PortalState(self._context)
4074

4175

4276
class PortalState(State):
4377
def electric(self) -> ElectricState:
78+
"""Navigate to the Electric entry point of the portal."""
4479
logger.info("Going to electric")
4580
self.driver.get("https://secure.rateacuity.com/RateAcuity/ElecEntry/IndexViews")
4681
return ElectricState(self._context)
4782

4883

4984
class ElectricState(State):
5085
def benchmark(self) -> ElectricBenchmarkStateDropdown:
86+
"""Switch the Electric report type to Benchmark and expose the state dropdown."""
5187
self._select_report("benchmark")
5288
return ElectricBenchmarkStateDropdown(self._context)
5389

5490
def _select_report(self, report: str):
91+
"""Click the given radio report selector if it is not already active."""
5592
radio = self._wait().until(
5693
EC.presence_of_element_located((By.XPATH, f'//input[@id="report" and @value="{report}"]'))
5794
)
@@ -64,11 +101,13 @@ def _wait_for_element(self):
64101
return self._wait().until(EC.presence_of_element_located((By.ID, "StateSelect")))
65102

66103
def get_states(self) -> list[str]:
104+
"""Return all available states visible in the State dropdown."""
67105
dropdown = self._wait_for_element()
68106
options = dropdown.find_elements(By.TAG_NAME, "option")
69107
return [_.text for _ in options]
70108

71109
def select_state(self, state: str) -> ElectricBenchmarkUtilityDropdown:
110+
"""Select the provided state and transition to the utility dropdown."""
72111
dropdown = self._wait_for_element()
73112
options = [_.text.strip() for _ in dropdown.find_elements(By.TAG_NAME, "option")]
74113
if state not in options:
@@ -86,11 +125,13 @@ def _wait_for_element(self):
86125
return self._wait().until(EC.presence_of_element_located((By.ID, "UtilitySelect")))
87126

88127
def get_utilities(self) -> list[str]:
128+
"""Return all available utilities for the previously chosen state."""
89129
dropdown = self._wait_for_element()
90130
options = dropdown.find_elements(By.TAG_NAME, "option")
91131
return [_.text for _ in options]
92132

93133
def select_utility(self, utility: str):
134+
"""Select the provided utility and expose the schedule dropdown."""
94135
dropdown = self._wait_for_element()
95136
options = [_.text.strip() for _ in dropdown.find_elements(By.TAG_NAME, "option")]
96137
if utility not in options:
@@ -108,11 +149,13 @@ def _wait_for_element(self):
108149
return self._wait().until(EC.presence_of_element_located((By.ID, "ScheduleSelect")))
109150

110151
def get_schedules(self) -> list[str]:
152+
"""Return all schedules associated with the selected utility."""
111153
dropdown = self._wait_for_element()
112154
options = dropdown.find_elements(By.TAG_NAME, "option")
113155
return [_.text for _ in options]
114156

115157
def select_schedule(self, schedule: str):
158+
"""Select a schedule and produce a report interface that can fetch data."""
116159
dropdown = self._wait_for_element()
117160
options = [_.text.strip() for _ in dropdown.find_elements(By.TAG_NAME, "option")]
118161
if schedule not in options:
@@ -127,10 +170,12 @@ def select_schedule(self, schedule: str):
127170

128171
class ElectricBenchmarkReport(State):
129172
def back_to_selections(self) -> ElectricBenchmarkScheduleDropdown:
173+
"""Return to the selections page so additional schedules can be fetched."""
130174
self._wait().until(EC.presence_of_element_located((By.LINK_TEXT, "Back To Selections"))).click()
131175
return ElectricBenchmarkScheduleDropdown(self._context)
132176

133177
def download_excel(self) -> Path:
178+
"""Trigger the report download and return the path once it appears."""
134179
self._wait().until(EC.presence_of_element_located((By.XPATH, '//a[text()="Create Excel Spreadsheet"]'))).click()
135180
download_path = self._context.download_path
136181
initial_state = _get_xlsx(download_path)
@@ -145,6 +190,7 @@ def download_excel(self) -> Path:
145190
return Path(download_path, filename)
146191

147192
def as_dataframe(self) -> pl.DataFrame:
193+
"""Convert a freshly downloaded Excel report into a cleaned Polars dataframe."""
148194
filepath = self.download_excel()
149195
logger.info(f"Reading excel file {filepath}")
150196
raw_data = pl.read_excel(filepath, engine="calamine", has_header=False)
@@ -162,10 +208,12 @@ def as_dataframe(self) -> pl.DataFrame:
162208

163209

164210
def _get_xlsx(folder) -> set[str]:
211+
"""Return the set of .xlsx filenames currently present in the provided folder."""
165212
return {_ for _ in os.listdir(folder) if _.endswith(".xlsx")}
166213

167214

168215
def main(argv: Sequence[str] | None = None):
216+
"""Fetch residential benchmark schedules for a hard-coded utility and state."""
169217
logging.basicConfig(level=logging.INFO)
170218

171219
parser = argparse.ArgumentParser(description="Fetch RateAcuity utility rates")

0 commit comments

Comments
 (0)