tariff_fetch 0.1__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.
- tariff_fetch/__init__.py +3 -0
- tariff_fetch/_cli/__init__.py +33 -0
- tariff_fetch/_cli/genability.py +152 -0
- tariff_fetch/_cli/openei.py +89 -0
- tariff_fetch/_cli/rateacuity.py +164 -0
- tariff_fetch/_cli/rateacuity_gas_urdb.py +138 -0
- tariff_fetch/_cli/types.py +67 -0
- tariff_fetch/arcadiaurdb_.py +0 -0
- tariff_fetch/cli.py +179 -0
- tariff_fetch/cli_gas.py +72 -0
- tariff_fetch/foo.py +6 -0
- tariff_fetch/genability/__init__.py +1 -0
- tariff_fetch/genability/base.py +24 -0
- tariff_fetch/genability/converters.py +9 -0
- tariff_fetch/genability/lse.py +70 -0
- tariff_fetch/genability/pagination.py +12 -0
- tariff_fetch/genability/response.py +437 -0
- tariff_fetch/genability/search.py +42 -0
- tariff_fetch/genability/tariffs.py +126 -0
- tariff_fetch/openei/__init__.py +0 -0
- tariff_fetch/openei/base.py +26 -0
- tariff_fetch/openei/utility_rates.py +347 -0
- tariff_fetch/openeia.py +47 -0
- tariff_fetch/rateacuity/__init__.py +2 -0
- tariff_fetch/rateacuity/base.py +67 -0
- tariff_fetch/rateacuity/report_tables.py +81 -0
- tariff_fetch/rateacuity/schema.py +220 -0
- tariff_fetch/rateacuity/state.py +515 -0
- tariff_fetch/rateacuity/urdb.py +12 -0
- tariff_fetch/urdb/__init__.py +0 -0
- tariff_fetch/urdb/rateacuity_gas.py +53 -0
- tariff_fetch/urdb/rateacuity_history_gas/__init__.py +78 -0
- tariff_fetch/urdb/rateacuity_history_gas/exceptions.py +15 -0
- tariff_fetch/urdb/rateacuity_history_gas/history_data.py +162 -0
- tariff_fetch/urdb/rateacuity_history_gas/shared.py +19 -0
- tariff_fetch/urdb/rateacuity_history_gas/types.py +36 -0
- tariff_fetch/urdb/schema.py +179 -0
- tariff_fetch-0.1.dist-info/METADATA +151 -0
- tariff_fetch-0.1.dist-info/RECORD +42 -0
- tariff_fetch-0.1.dist-info/WHEEL +4 -0
- tariff_fetch-0.1.dist-info/entry_points.txt +3 -0
- tariff_fetch-0.1.dist-info/licenses/LICENSE +21 -0
tariff_fetch/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
from pathvalidate import sanitize_filename
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def prompt_filename(output_folder: Path, suggested_filename: str, extension: str) -> Path:
|
|
13
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
14
|
+
suggested_filename = sanitize_filename(f"{suggested_filename}_{date_str}")
|
|
15
|
+
if output_folder.exists():
|
|
16
|
+
existing_filenames = set(output_folder.iterdir())
|
|
17
|
+
filepath = next(
|
|
18
|
+
_
|
|
19
|
+
for i in range(0xFFFFFF)
|
|
20
|
+
if (_ := output_folder.joinpath(f"{suggested_filename}-{i}{os.extsep}{extension}"))
|
|
21
|
+
not in existing_filenames
|
|
22
|
+
)
|
|
23
|
+
else:
|
|
24
|
+
filepath = output_folder.joinpath(f"{suggested_filename}-0{os.extsep}{extension}")
|
|
25
|
+
|
|
26
|
+
return Path(
|
|
27
|
+
questionary.path(
|
|
28
|
+
message="Path to save the results",
|
|
29
|
+
default=filepath.as_posix(),
|
|
30
|
+
file_filter=lambda _: Path(_).suffix == extension,
|
|
31
|
+
validate=lambda _: (not os.path.exists(_)) or "A file with that name already exists",
|
|
32
|
+
).ask()
|
|
33
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import questionary
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
from tariff_fetch.genability.lse import get_lses_page
|
|
10
|
+
from tariff_fetch.genability.tariffs import CustomerClass, TariffType, tariffs_paginate
|
|
11
|
+
|
|
12
|
+
from . import console, prompt_filename
|
|
13
|
+
from .types import Utility
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_utility_lse_id(utility: Utility) -> int | None:
|
|
17
|
+
with console.status("Fetching lses..."):
|
|
18
|
+
lses = get_lses_page(
|
|
19
|
+
fields="min",
|
|
20
|
+
searchOn=["code"],
|
|
21
|
+
search=str(utility.eia_id),
|
|
22
|
+
startsWith=True,
|
|
23
|
+
endsWith=True,
|
|
24
|
+
)["results"]
|
|
25
|
+
if len(lses) == 0:
|
|
26
|
+
# No utilities found with this eia id
|
|
27
|
+
console.print(
|
|
28
|
+
f'Utility "{utility.name}" with EIA Id {utility.eia_id} not found in arcadia database', style="bold red"
|
|
29
|
+
)
|
|
30
|
+
return None
|
|
31
|
+
if len(lses) == 1:
|
|
32
|
+
# Found one utility
|
|
33
|
+
utility_lse_id = lses[0]["lseId"]
|
|
34
|
+
return utility_lse_id
|
|
35
|
+
else:
|
|
36
|
+
# Nothing found; this should *theoretically* never happen but let's keep it just in case
|
|
37
|
+
choices = [questionary.Choice(title=_["name"], value=_["lseId"]) for _ in lses]
|
|
38
|
+
choices.append(questionary.Separator())
|
|
39
|
+
choices.append(questionary.Choice(title="None of these", value=None))
|
|
40
|
+
utility_lse_id: int | None = questionary.select(
|
|
41
|
+
message=f"Found multiple utilities with lse id = {utility.eia_id}. Select one.", choices=choices
|
|
42
|
+
).ask()
|
|
43
|
+
if utility_lse_id is None:
|
|
44
|
+
console.print("No utility chosen")
|
|
45
|
+
return None
|
|
46
|
+
return utility_lse_id
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _select_tariffs(
|
|
50
|
+
lse_id: int, customer_classes: list[CustomerClass], tariff_types: list[TariffType]
|
|
51
|
+
) -> list[tuple[str, int]]:
|
|
52
|
+
with console.status("Fetching tariffs..."):
|
|
53
|
+
tariffs = list(
|
|
54
|
+
tariffs_paginate(
|
|
55
|
+
lseId=lse_id,
|
|
56
|
+
fields="min",
|
|
57
|
+
effectiveOn=datetime.now(timezone.utc),
|
|
58
|
+
customerClasses=customer_classes,
|
|
59
|
+
tariffTypes=tariff_types,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
if not tariffs:
|
|
63
|
+
return []
|
|
64
|
+
return questionary.checkbox(
|
|
65
|
+
message="Select tariffs",
|
|
66
|
+
choices=[
|
|
67
|
+
questionary.Choice(title=_["tariffName"], value=(_["tariffName"], _["masterTariffId"]), checked=True)
|
|
68
|
+
for _ in tariffs
|
|
69
|
+
],
|
|
70
|
+
use_search_filter=True,
|
|
71
|
+
use_jk_keys=False,
|
|
72
|
+
).ask()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _select_customer_classes() -> list[CustomerClass]:
|
|
76
|
+
return questionary.checkbox(
|
|
77
|
+
message="Select customer classes",
|
|
78
|
+
choices=[
|
|
79
|
+
questionary.Choice(title="Residential", value="RESIDENTIAL"),
|
|
80
|
+
questionary.Choice(title="General", value="GENERAL"),
|
|
81
|
+
questionary.Choice(title="Special Use", value="SPECIAL_USE"),
|
|
82
|
+
],
|
|
83
|
+
validate=lambda _: True if _ else "Select at least one customer class",
|
|
84
|
+
).ask()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _select_tariff_types() -> list[TariffType]:
|
|
88
|
+
return questionary.checkbox(
|
|
89
|
+
message="Select tariff types",
|
|
90
|
+
choices=[
|
|
91
|
+
questionary.Choice(title="Default", value="DEFAULT"),
|
|
92
|
+
questionary.Choice(title="Alternative", value="ALTERNATIVE"),
|
|
93
|
+
questionary.Choice(title="Optional extra", value="OPTIONAL_EXTRA"),
|
|
94
|
+
questionary.Choice(title="Rider", value="RIDER"),
|
|
95
|
+
],
|
|
96
|
+
validate=lambda _: bool(_) or "Select at least one tariff type",
|
|
97
|
+
).ask()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _fetch_tariffs(tariffs: list[tuple[str, int]]):
|
|
101
|
+
result = []
|
|
102
|
+
with console.status("Fetching tariffs..."):
|
|
103
|
+
for name, id_ in tariffs:
|
|
104
|
+
console.print(f"Tariff id: {name}")
|
|
105
|
+
page = list(
|
|
106
|
+
tariffs_paginate(
|
|
107
|
+
masterTariffId=id_,
|
|
108
|
+
effectiveOn=datetime.now(timezone.utc),
|
|
109
|
+
fields="ext",
|
|
110
|
+
populateProperties=True,
|
|
111
|
+
populateRates=True,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
result.extend(page)
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def process_genability(utility: Utility, output_folder: Path):
|
|
119
|
+
load_dotenv()
|
|
120
|
+
if not os.getenv("ARCADIA_APP_ID"):
|
|
121
|
+
console.print("[b]ARCADIA_APP_ID[/] environment variable is not set.")
|
|
122
|
+
if not os.getenv("ARCADIA_APP_KEY"):
|
|
123
|
+
console.print("[b]ARCADIA_APP_KEY[/] environment variable is not set.")
|
|
124
|
+
if not (os.getenv("ARCADIA_APP_ID") and os.getenv("ARCADIA_APP_KEY")):
|
|
125
|
+
console.print("Cannot use Arcadia API due to missing credentials")
|
|
126
|
+
console.input("Press enter to proceed...")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
lse_id = _find_utility_lse_id(utility)
|
|
130
|
+
if lse_id is None:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
if not (customer_classes := _select_customer_classes()):
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if not (tariff_types := _select_tariff_types()):
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if not (tariffs := _select_tariffs(lse_id, customer_classes, tariff_types)):
|
|
140
|
+
console.print("[red]No tariffs found[/]")
|
|
141
|
+
console.input("Press enter to proceed...")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
results = _fetch_tariffs(tariffs)
|
|
145
|
+
suggested_filename = f"arcadia_{utility.name}"
|
|
146
|
+
|
|
147
|
+
if not (filename := prompt_filename(output_folder, suggested_filename, "json")):
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
filename.parent.mkdir(exist_ok=True)
|
|
151
|
+
filename.write_text(json.dumps(results, indent=2))
|
|
152
|
+
console.print(f"Wrote [blue]{len(results)}[/] records to {filename}")
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import questionary
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
from tariff_fetch.openei.utility_rates import UtilityRateSector, UtilityRatesResponseItem, iter_utility_rates
|
|
11
|
+
|
|
12
|
+
from . import console, prompt_filename
|
|
13
|
+
from .types import Utility
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _prompt_sector() -> UtilityRateSector:
|
|
17
|
+
return questionary.select(
|
|
18
|
+
message="Select sector",
|
|
19
|
+
choices=[
|
|
20
|
+
"Residential",
|
|
21
|
+
"Commercial",
|
|
22
|
+
"Industrial",
|
|
23
|
+
"Lighting",
|
|
24
|
+
],
|
|
25
|
+
).ask()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _prompt_detail_level() -> Literal["full", "minimal"]:
|
|
29
|
+
return questionary.select(
|
|
30
|
+
message="Select level of detail",
|
|
31
|
+
choices=["full", "minimal"],
|
|
32
|
+
).ask()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_tariffs(
|
|
36
|
+
eia_id: int, sector: UtilityRateSector, detail: Literal["full", "minimal"]
|
|
37
|
+
) -> list[UtilityRatesResponseItem]:
|
|
38
|
+
api_key = os.getenv("OPENEI_API_KEY")
|
|
39
|
+
if not api_key:
|
|
40
|
+
raise ValueError("API Key is not set (via OPENEI_API_KEY variable)")
|
|
41
|
+
with console.status("Fetching rates..."):
|
|
42
|
+
iterator = iter_utility_rates(
|
|
43
|
+
api_key,
|
|
44
|
+
effective_on_date=datetime.now(timezone.utc),
|
|
45
|
+
sector=sector,
|
|
46
|
+
detail=detail,
|
|
47
|
+
eia=eia_id,
|
|
48
|
+
)
|
|
49
|
+
return list(iterator)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _prompt_tariffs(tariffs: list[UtilityRatesResponseItem]) -> list[UtilityRatesResponseItem]:
|
|
53
|
+
return questionary.checkbox(
|
|
54
|
+
message="Select tariffs to include",
|
|
55
|
+
choices=[questionary.Choice(title=_["name"], value=_, checked=True) for _ in tariffs],
|
|
56
|
+
).ask()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def process_openei(utility: Utility, output_folder: Path):
|
|
60
|
+
load_dotenv()
|
|
61
|
+
if not os.getenv("OPENEI_API_KEY"):
|
|
62
|
+
console.print("[b]OPENEI_API_KEY[/] environment variable is not set")
|
|
63
|
+
console.print("Cannot use OpenEI API due to missing credentials")
|
|
64
|
+
console.input("Press enter to proceed...")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if not (sector := _prompt_sector()):
|
|
68
|
+
return
|
|
69
|
+
if not (detail_level := _prompt_detail_level()):
|
|
70
|
+
return
|
|
71
|
+
tariffs = _get_tariffs(utility.eia_id, sector, detail_level)
|
|
72
|
+
if not tariffs:
|
|
73
|
+
console.print("[red]No tariffs found[/]")
|
|
74
|
+
console.input("Press enter to proceed...")
|
|
75
|
+
return
|
|
76
|
+
tariffs = _prompt_tariffs(tariffs)
|
|
77
|
+
if not tariffs:
|
|
78
|
+
console.print("[red]No tariffs selected[/]")
|
|
79
|
+
console.input("Press enter to proceed...")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
suggested_filename = f"openei_{utility.name}_{sector}_{detail_level}"
|
|
83
|
+
if not (filepath := prompt_filename(output_folder, suggested_filename, "json")):
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
filepath.parent.mkdir(exist_ok=True)
|
|
87
|
+
print(filepath)
|
|
88
|
+
filepath.write_text(json.dumps(tariffs, indent=2))
|
|
89
|
+
console.print(f"Wrote [blue]{len(tariffs)}[/] items to {filepath}")
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
import tenacity
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
from fuzzywuzzy import fuzz
|
|
9
|
+
from selenium.common.exceptions import WebDriverException
|
|
10
|
+
|
|
11
|
+
from tariff_fetch._cli.types import Utility
|
|
12
|
+
from tariff_fetch.rateacuity import LoginState, create_context
|
|
13
|
+
|
|
14
|
+
from . import console, prompt_filename
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def process_rateacuity_gas(output_folder: Path, state: str):
|
|
18
|
+
load_dotenv()
|
|
19
|
+
if not (username := os.getenv("RATEACUITY_USERNAME")):
|
|
20
|
+
console.print("[b]RATEACUITY_USERNAME[/] environment variable is not set")
|
|
21
|
+
if not (password := os.getenv("RATEACUITY_PASSWORD")):
|
|
22
|
+
console.print("[b]RATEACUITY_PASSWORD[/] environment variable is not set")
|
|
23
|
+
if not (username and password):
|
|
24
|
+
console.print("Cannot use RateAcuity due to missing credentials")
|
|
25
|
+
console.input("Press enter to proceed...")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
selected_utility = None
|
|
29
|
+
tariffs_to_include = None
|
|
30
|
+
results = []
|
|
31
|
+
|
|
32
|
+
for attempt in tenacity.Retrying(
|
|
33
|
+
stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(WebDriverException)
|
|
34
|
+
):
|
|
35
|
+
with attempt, create_context() as context:
|
|
36
|
+
with console.status("Fetching list of utilities..."):
|
|
37
|
+
scraping_state = (
|
|
38
|
+
LoginState(context).login(username, password).gas().benchmark_all().select_state(state.upper())
|
|
39
|
+
)
|
|
40
|
+
utilities = [_ for _ in scraping_state.get_utilities() if _]
|
|
41
|
+
|
|
42
|
+
if not utilities:
|
|
43
|
+
raise RuntimeError(f"Something's wrong: rateacuity shows no utilities for this state ({state})")
|
|
44
|
+
|
|
45
|
+
if selected_utility is None:
|
|
46
|
+
selected_utility = questionary.select(
|
|
47
|
+
message="Select a utility from available choices",
|
|
48
|
+
choices=utilities,
|
|
49
|
+
use_jk_keys=False,
|
|
50
|
+
use_search_filter=True,
|
|
51
|
+
use_shortcuts=False,
|
|
52
|
+
).ask()
|
|
53
|
+
if not selected_utility:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
with console.status("Fetching list of tariffs..."):
|
|
57
|
+
scraping_state = scraping_state.select_utility(selected_utility)
|
|
58
|
+
tariffs = [_ for _ in scraping_state.get_schedules() if _]
|
|
59
|
+
|
|
60
|
+
if tariffs_to_include is None:
|
|
61
|
+
tariffs_to_include = questionary.checkbox(
|
|
62
|
+
message="Select tariffs to include",
|
|
63
|
+
choices=tariffs,
|
|
64
|
+
use_jk_keys=False,
|
|
65
|
+
use_search_filter=True,
|
|
66
|
+
validate=lambda _: bool(_) or "Select at least one tariff",
|
|
67
|
+
).ask()
|
|
68
|
+
|
|
69
|
+
if not tariffs_to_include:
|
|
70
|
+
console.print("[red]No tariffs selected[/]")
|
|
71
|
+
console.input("Press enter to proceed...")
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
with console.status("Fetching tariffs..."):
|
|
75
|
+
while tariffs_to_include:
|
|
76
|
+
tariff = tariffs_to_include.pop(0)
|
|
77
|
+
console.log(f"Fetching {tariff}")
|
|
78
|
+
scraping_state = scraping_state.select_schedule(tariff)
|
|
79
|
+
sections = scraping_state.as_sections()
|
|
80
|
+
results.append({"schedule": tariff, "sections": sections})
|
|
81
|
+
scraping_state = scraping_state.back_to_selections()
|
|
82
|
+
|
|
83
|
+
assert selected_utility
|
|
84
|
+
suggested_filename = f"gas_rateacuity_{selected_utility}"
|
|
85
|
+
filename = prompt_filename(output_folder, suggested_filename, "json")
|
|
86
|
+
filename.parent.mkdir(exist_ok=True)
|
|
87
|
+
filename.write_text(json.dumps(results, indent=2))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def process_rateacuity(output_folder: Path, state: str, utility: Utility):
|
|
91
|
+
load_dotenv()
|
|
92
|
+
if not (username := os.getenv("RATEACUITY_USERNAME")):
|
|
93
|
+
console.print("[b]RATEACUITY_USERNAME[/] environment variable is not set")
|
|
94
|
+
if not (password := os.getenv("RATEACUITY_PASSWORD")):
|
|
95
|
+
console.print("[b]RATEACUITY_PASSWORD[/] environment variable is not set")
|
|
96
|
+
if not (username and password):
|
|
97
|
+
console.print("Cannot use RateAcuity due to missing credentials")
|
|
98
|
+
console.input("Press enter to proceed...")
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
selected_utility = None
|
|
102
|
+
tariffs_to_include = None
|
|
103
|
+
results = []
|
|
104
|
+
|
|
105
|
+
for attempt in tenacity.Retrying(
|
|
106
|
+
stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(WebDriverException)
|
|
107
|
+
):
|
|
108
|
+
with attempt, create_context() as context:
|
|
109
|
+
with console.status("Fetching list of utilities..."):
|
|
110
|
+
scraping_state = (
|
|
111
|
+
LoginState(context).login(username, password).electric().benchmark_all().select_state(state.upper())
|
|
112
|
+
)
|
|
113
|
+
utilities = [_ for _ in scraping_state.get_utilities() if _]
|
|
114
|
+
|
|
115
|
+
if not utilities:
|
|
116
|
+
raise RuntimeError(f"Something's wrong: rateacuity shows no utilities for this state ({state})")
|
|
117
|
+
|
|
118
|
+
if selected_utility is None:
|
|
119
|
+
utilities_scored = sorted(utilities, key=lambda _: fuzz.ratio(utility.name, _), reverse=True)
|
|
120
|
+
selected_utility = utilities_scored.pop(0)
|
|
121
|
+
if not questionary.confirm(f"Is this the correct utility: {selected_utility} ?").ask():
|
|
122
|
+
selected_utility = questionary.select(
|
|
123
|
+
message="Select a utility from available choices",
|
|
124
|
+
choices=utilities_scored,
|
|
125
|
+
use_jk_keys=False,
|
|
126
|
+
use_search_filter=True,
|
|
127
|
+
use_shortcuts=False,
|
|
128
|
+
).ask()
|
|
129
|
+
if not selected_utility:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
with console.status("Fetching list of tariffs..."):
|
|
133
|
+
scraping_state = scraping_state.select_utility(selected_utility)
|
|
134
|
+
tariffs = [_ for _ in scraping_state.get_schedules() if _]
|
|
135
|
+
|
|
136
|
+
if tariffs_to_include is None:
|
|
137
|
+
tariffs_to_include = questionary.checkbox(
|
|
138
|
+
message="Select tariffs to include",
|
|
139
|
+
choices=tariffs,
|
|
140
|
+
use_jk_keys=False,
|
|
141
|
+
use_search_filter=True,
|
|
142
|
+
validate=lambda _: bool(_) or "Select at least one tariff",
|
|
143
|
+
).ask()
|
|
144
|
+
|
|
145
|
+
if not tariffs_to_include:
|
|
146
|
+
console.print("[red]No tariffs selected[/]")
|
|
147
|
+
console.input("Press enter to proceed...")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
with console.status("Fetching tariffs..."):
|
|
151
|
+
while tariffs_to_include:
|
|
152
|
+
tariff = tariffs_to_include.pop(0)
|
|
153
|
+
console.log(f"Fetching {tariff}")
|
|
154
|
+
scraping_state = scraping_state.select_schedule(tariff)
|
|
155
|
+
sections = scraping_state.as_sections()
|
|
156
|
+
results.append({"schedule": tariff, "sections": sections})
|
|
157
|
+
scraping_state = scraping_state.back_to_selections()
|
|
158
|
+
|
|
159
|
+
assert selected_utility
|
|
160
|
+
suggested_filename = f"rateacuity_{selected_utility}"
|
|
161
|
+
if not (filename := prompt_filename(output_folder, suggested_filename, "json")):
|
|
162
|
+
return
|
|
163
|
+
filename.parent.mkdir(exist_ok=True)
|
|
164
|
+
filename.write_text(json.dumps(results, indent=2))
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import Collection
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from statistics import mean
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
import questionary
|
|
10
|
+
import tenacity
|
|
11
|
+
from dotenv import load_dotenv
|
|
12
|
+
from rich.prompt import Confirm
|
|
13
|
+
from selenium.common.exceptions import WebDriverException
|
|
14
|
+
|
|
15
|
+
from tariff_fetch.rateacuity import LoginState, create_context
|
|
16
|
+
from tariff_fetch.urdb.rateacuity_history_gas import (
|
|
17
|
+
build_urdb,
|
|
18
|
+
)
|
|
19
|
+
from tariff_fetch.urdb.rateacuity_history_gas.history_data import HistoryData, PercentageRow, Row
|
|
20
|
+
from tariff_fetch.urdb.schema import URDBRate
|
|
21
|
+
|
|
22
|
+
from . import console, prompt_filename
|
|
23
|
+
|
|
24
|
+
# TODO: This is ungodly ugly but it works
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def process_rateacuity_gas_urdb(output_folder: Path, state: str, year: int):
|
|
28
|
+
_ = load_dotenv()
|
|
29
|
+
if not (username := os.getenv("RATEACUITY_USERNAME")):
|
|
30
|
+
console.print("[b]RATEACUITY_USERNAME[/] environment variable is not set")
|
|
31
|
+
if not (password := os.getenv("RATEACUITY_PASSWORD")):
|
|
32
|
+
console.print("[b]RATEACUITY_PASSWORD[/] environment variable is not set")
|
|
33
|
+
if not (username and password):
|
|
34
|
+
console.print("Cannot use RateAcuity due to missing credentials")
|
|
35
|
+
_ = console.input("Press enter to proceed...")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
selected_utility = None
|
|
39
|
+
tariffs_to_include = None
|
|
40
|
+
result: list[URDBRate] = []
|
|
41
|
+
for attempt in tenacity.Retrying(
|
|
42
|
+
stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(WebDriverException)
|
|
43
|
+
):
|
|
44
|
+
with attempt, create_context() as context:
|
|
45
|
+
with console.status("Fetching list of utilities..."):
|
|
46
|
+
scraping_state = (
|
|
47
|
+
LoginState(context).login(username, password).gas().history().select_state(state.upper())
|
|
48
|
+
)
|
|
49
|
+
utilities = [_ for _ in scraping_state.get_utilities() if _]
|
|
50
|
+
if selected_utility is None:
|
|
51
|
+
selected_utility = cast(
|
|
52
|
+
str,
|
|
53
|
+
questionary.select(
|
|
54
|
+
message="Select a utility from available choices",
|
|
55
|
+
choices=utilities,
|
|
56
|
+
use_jk_keys=False,
|
|
57
|
+
use_search_filter=True,
|
|
58
|
+
use_shortcuts=False,
|
|
59
|
+
).ask(),
|
|
60
|
+
)
|
|
61
|
+
if not selected_utility:
|
|
62
|
+
return
|
|
63
|
+
with console.status("Fetching list of tariffs..."):
|
|
64
|
+
scraping_state = scraping_state.select_utility(selected_utility)
|
|
65
|
+
tariffs = [_ for _ in scraping_state.get_schedules() if _]
|
|
66
|
+
if tariffs_to_include is None:
|
|
67
|
+
tariffs_to_include = cast(
|
|
68
|
+
list[str],
|
|
69
|
+
questionary.checkbox(
|
|
70
|
+
message="Select tariffs to include",
|
|
71
|
+
choices=tariffs,
|
|
72
|
+
use_jk_keys=False,
|
|
73
|
+
use_search_filter=True,
|
|
74
|
+
validate=lambda _: bool(_) or "Select at least one tariff",
|
|
75
|
+
).ask(),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not tariffs_to_include:
|
|
79
|
+
console.print("[red]No tariffs selected[/]")
|
|
80
|
+
_ = console.input("Press enter to proceed...")
|
|
81
|
+
return
|
|
82
|
+
console.print("Fetching tariffs")
|
|
83
|
+
while tariffs_to_include:
|
|
84
|
+
tariff = tariffs_to_include.pop(0)
|
|
85
|
+
console.log(f"Fetching {tariff}")
|
|
86
|
+
scraping_state = (
|
|
87
|
+
scraping_state.select_schedule(tariff)
|
|
88
|
+
.set_enddate(date(year, 12, 1))
|
|
89
|
+
.set_number_of_comparisons(12)
|
|
90
|
+
.set_frequency(1)
|
|
91
|
+
)
|
|
92
|
+
df = scraping_state.as_dataframe()
|
|
93
|
+
hd = HistoryData(df)
|
|
94
|
+
validation_errors = hd.validate_rows()
|
|
95
|
+
proceed = True
|
|
96
|
+
if validation_errors:
|
|
97
|
+
console.print("Following rows cannot be processed and will be ignored:")
|
|
98
|
+
for error in validation_errors:
|
|
99
|
+
console.print(f" - {error.row}")
|
|
100
|
+
proceed = Confirm.ask("Proceed?", console=console)
|
|
101
|
+
|
|
102
|
+
if proceed:
|
|
103
|
+
apply_percentages = False
|
|
104
|
+
rows = list(hd.rows())
|
|
105
|
+
if percentage_columns := _get_percentage_columns(rows):
|
|
106
|
+
percentage_columns_strings = [
|
|
107
|
+
f"- {c[0]} ({c[1]}): {c[2]}" if c[1] else f"- {c[0]}: {c[2]}" for c in percentage_columns
|
|
108
|
+
]
|
|
109
|
+
console.print("Found following percentage columns (values are averages over 12 months):")
|
|
110
|
+
console.print("\n".join(percentage_columns_strings))
|
|
111
|
+
console.print("It is impossible to tell which percentages apply to which specific rates.")
|
|
112
|
+
console.print("Percentages will be applied to the final result as is")
|
|
113
|
+
apply_percentages = Confirm.ask("Apply percentages? (otherwise percentages will be ignored)")
|
|
114
|
+
|
|
115
|
+
urdb = build_urdb(rows, apply_percentages)
|
|
116
|
+
urdb["utility"] = selected_utility
|
|
117
|
+
urdb["name"] = tariff
|
|
118
|
+
result.append(urdb)
|
|
119
|
+
|
|
120
|
+
scraping_state = (
|
|
121
|
+
scraping_state.back_to_selections()
|
|
122
|
+
.history()
|
|
123
|
+
.select_state(state.upper())
|
|
124
|
+
.select_utility(selected_utility)
|
|
125
|
+
)
|
|
126
|
+
suggested_filename = f"rateacuity_{selected_utility}.urdb.{year}."
|
|
127
|
+
if not (filename := prompt_filename(output_folder, suggested_filename, "json")):
|
|
128
|
+
return
|
|
129
|
+
filename.parent.mkdir(exist_ok=True)
|
|
130
|
+
_ = filename.write_text(json.dumps(result, indent=2))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _get_percentage_columns(rows: Collection[Row]) -> list[tuple[str, str | None, float]]:
|
|
134
|
+
return [
|
|
135
|
+
(row.rate, row.location, mean(row.month_value_float(month) for month in range(0, 12)))
|
|
136
|
+
for row in rows
|
|
137
|
+
if isinstance(row, PercentageRow)
|
|
138
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Provider(str, Enum):
|
|
6
|
+
GENABILITY = "Genability"
|
|
7
|
+
OPENEI = "OpenEI"
|
|
8
|
+
RATEACUITY = "RateAcuity"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StateCode(str, Enum):
|
|
12
|
+
AL = "al"
|
|
13
|
+
AK = "ak"
|
|
14
|
+
AZ = "az"
|
|
15
|
+
AR = "ar"
|
|
16
|
+
CA = "ca"
|
|
17
|
+
CO = "co"
|
|
18
|
+
CT = "ct"
|
|
19
|
+
DE = "de"
|
|
20
|
+
FL = "fl"
|
|
21
|
+
GA = "ga"
|
|
22
|
+
HI = "hi"
|
|
23
|
+
ID = "id"
|
|
24
|
+
IL = "il"
|
|
25
|
+
IN = "in"
|
|
26
|
+
IA = "ia"
|
|
27
|
+
KS = "ks"
|
|
28
|
+
KY = "ky"
|
|
29
|
+
LA = "la"
|
|
30
|
+
ME = "me"
|
|
31
|
+
MD = "md"
|
|
32
|
+
MA = "ma"
|
|
33
|
+
MI = "mi"
|
|
34
|
+
MN = "mn"
|
|
35
|
+
MS = "ms"
|
|
36
|
+
MO = "mo"
|
|
37
|
+
MT = "mt"
|
|
38
|
+
NE = "ne"
|
|
39
|
+
NV = "nv"
|
|
40
|
+
NH = "nh"
|
|
41
|
+
NJ = "nj"
|
|
42
|
+
NM = "nm"
|
|
43
|
+
NY = "ny"
|
|
44
|
+
NC = "nc"
|
|
45
|
+
ND = "nd"
|
|
46
|
+
OH = "oh"
|
|
47
|
+
OK = "ok"
|
|
48
|
+
OR = "or"
|
|
49
|
+
PA = "pa"
|
|
50
|
+
RI = "ri"
|
|
51
|
+
SC = "sc"
|
|
52
|
+
SD = "sd"
|
|
53
|
+
TN = "tn"
|
|
54
|
+
TX = "tx"
|
|
55
|
+
UT = "ut"
|
|
56
|
+
VT = "vt"
|
|
57
|
+
VA = "va"
|
|
58
|
+
WA = "wa"
|
|
59
|
+
WV = "wv"
|
|
60
|
+
WI = "wi"
|
|
61
|
+
WY = "wy"
|
|
62
|
+
DC = "dc"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Utility(NamedTuple):
|
|
66
|
+
eia_id: int
|
|
67
|
+
name: str
|
|
File without changes
|