Skip to content

Commit

Permalink
Added greenbutton_objects to parse XML versions of natural gas and el…
Browse files Browse the repository at this point in the history
…ectric bills, alongside pydantic models and tests for the same.
  • Loading branch information
Vinyl authored and Vinyl committed Jul 3, 2024
1 parent 9033c6f commit 31d6e9a
Show file tree
Hide file tree
Showing 9 changed files with 2,842 additions and 15 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ node_modules
# Easy way to create temporary files/folders that won't accidentally be added to git
*.local.*

# PyCharm
.idea/

#local temporary folders
heat-app
venv
Expand Down
3 changes: 2 additions & 1 deletion rules-engine/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ name="rules-engine"
version="0.0.1"
requires-python=">=3.11.3"
dependencies = [
"pydantic"
"pydantic",
"greenbutton_objects"
]

[project.optional-dependencies]
Expand Down
2 changes: 2 additions & 0 deletions rules-engine/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#
annotated-types==0.5.0
# via pydantic
greenbutton-objects==2024.7.3
# via rules-engine (pyproject.toml)
pydantic==2.3.0
# via rules-engine (pyproject.toml)
pydantic-core==2.6.3
Expand Down
56 changes: 55 additions & 1 deletion rules-engine/src/rules_engine/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
from datetime import datetime, timedelta
from enum import StrEnum

from .pydantic_models import NaturalGasBillingInput, NaturalGasBillingRecordInput
from greenbutton_objects import parse
from .pydantic_models import (
NaturalGasBillingInput,
NaturalGasBillingRecordInput,
ElectricBillingInput,
ElectricBillingRecordInput
)


class NaturalGasCompany(StrEnum):
Expand Down Expand Up @@ -177,3 +183,51 @@ def _parse_gas_bill_national_grid(data: str) -> NaturalGasBillingInput:
records.append(record)

return NaturalGasBillingInput(records=records)


def _parse_gas_bill_xml(path: str) -> NaturalGasBillingInput:
"""
Return a list of gas bill data parsed from a Green Button XML
received as a string.
"""
ups = parse.parse_feed(path)
records = []
for up in ups:
for mr in up.meterReadings:
for ir in mr.intervalReadings:
records.append(
NaturalGasBillingRecordInput(
period_start_date=ir.timePeriod.start.date(),
period_end_date=(
ir.timePeriod.start + ir.timePeriod.duration
).date(),
usage_therms=ir.value,
inclusion_override=None,
)
)

return NaturalGasBillingInput(records=records)


def _parse_electric_bill_xml(path: str) -> NaturalGasBillingInput:
"""
Return a list of gas bill data parsed from a Green Button XML
received as a string.
"""
ups = parse.parse_feed(path)
records = []
for up in ups:
for mr in up.meterReadings:
for ir in mr.intervalReadings:
records.append(
ElectricBillingRecordInput(
period_start_date=ir.timePeriod.start.date(),
period_end_date=(
ir.timePeriod.start + ir.timePeriod.duration
).date(),
usage_watt_hours=ir.value,
inclusion_override=None,
)
)

return ElectricBillingInput(records=records)
59 changes: 53 additions & 6 deletions rules-engine/src/rules_engine/pydantic_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,53 @@ class NaturalGasBillingRecordInput(BaseModel):
inclusion_override: Optional[AnalysisType] = Field(description="Natural Gas!E")


class ElectricBillingRecordInput(BaseModel):
"""From Electric tab. A single row of the Billing input table."""

period_start_date: date = Field(description="Electric!A")
period_end_date: date = Field(description="Electric!B")
usage_watt_hours: float = Field(description="Electric!D")
inclusion_override: Optional[AnalysisType] = Field(description="Electric!E")


class ElectricBillingInput(BaseModel):
"""From Electric tab. Container for holding all rows of the billing input table."""

records: Sequence[ElectricBillingRecordInput]

# Suppress mypy error when computed_field is used with cached_property; see https://github.com/python/mypy/issues/1362
@computed_field # type: ignore[misc]
@cached_property
def overall_start_date(self) -> date:
if len(self.records) == 0:
raise ValueError(
"Electric billing records cannot be empty.\n"
+ "Could not calculate overall start date from empty electricicity billing records.\n"
+ "Try again with non-empty electricicity billing records.\n"
)

min_date = date.max
for record in self.records:
min_date = min(min_date, record.period_start_date)
return min_date

# Suppress mypy error when computed_field is used with cached_property; see https://github.com/python/mypy/issues/1362
@computed_field # type: ignore[misc]
@cached_property
def overall_end_date(self) -> date:
if len(self.records) == 0:
raise ValueError(
"Electric billing records cannot be empty.\n"
+ "Could not calculate overall start date from empty electricicity billing records.\n"
+ "Try again with non-empty electricicity billing records.\n"
)

max_date = date.min
for record in self.records:
max_date = max(max_date, record.period_end_date)
return max_date


class NaturalGasBillingInput(BaseModel):
"""From Natural Gas tab. Container for holding all rows of the billing input table."""

Expand All @@ -113,9 +160,9 @@ class NaturalGasBillingInput(BaseModel):
def overall_start_date(self) -> date:
if len(self.records) == 0:
raise ValueError(
"Natural gas billing records cannot be empty."
+ "Could not calculate overall start date from empty natural gas billing records."
+ "Try again with non-empty natural gas billing records."
"Natural gas billing records cannot be empty.\n"
+ "Could not calculate overall start date from empty natural gas billing records.\n"
+ "Try again with non-empty natural gas billing records.\n"
)

min_date = date.max
Expand All @@ -129,9 +176,9 @@ def overall_start_date(self) -> date:
def overall_end_date(self) -> date:
if len(self.records) == 0:
raise ValueError(
"Natural gas billing records cannot be empty."
+ "Could not calculate overall start date from empty natural gas billing records."
+ "Try again with non-empty natural gas billing records."
"Natural gas billing records cannot be empty.\n"
+ "Could not calculate overall start date from empty natural gas billing records.\n"
+ "Try again with non-empty natural gas billing records.\n"
)

max_date = date.min
Expand Down
Loading

0 comments on commit 31d6e9a

Please sign in to comment.