Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add GetCities, GetCityStreets, GetOutagesByAddress APIs #99

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions iec_api/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@
GET_BILLING_INVOICES_URL = IEC_API_BASE_URL + "BillingCollection/invoices/{contract_id}/{bp_number}"
GET_INVOICE_PDF_URL = IEC_API_BASE_URL + "BillingCollection/pdf"
GET_KWH_TARIFF_URL = IEC_API_BASE_URL + "content/en-US/content/tariffs/contentpages/homeelectricitytariff"
GET_CITIES_URL = IEC_API_BASE_URL + "District/cities"
GET_CITY_STREETS_URL = IEC_API_BASE_URL + "District/streets/{city_id}"
GET_OUTAGES_BY_ADDRESS_URL = IEC_API_BASE_URL + "outages/transactionsByAddress"

ERROR_FIELD_NAME = "Error"
ERROR_SUMMARY_FIELD_NAME = "errorSummary"
95 changes: 91 additions & 4 deletions iec_api/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
from datetime import datetime
from typing import List, Optional, TypeVar
from uuid import UUID

from aiohttp import ClientSession
from mashumaro.codecs import BasicDecoder
Expand All @@ -11,6 +12,8 @@
GET_ACCOUNTS_URL,
GET_BILLING_INVOICES_URL,
GET_CHECK_CONTRACT_URL,
GET_CITIES_URL,
GET_CITY_STREETS_URL,
GET_CONSUMER_URL,
GET_CONTRACTS_URL,
GET_DEFAULT_CONTRACT_URL,
Expand All @@ -22,12 +25,16 @@
GET_INVOICE_PDF_URL,
GET_KWH_TARIFF_URL,
GET_LAST_METER_READING_URL,
GET_OUTAGES_BY_ADDRESS_URL,
GET_REQUEST_READING_URL,
GET_TENANT_IDENTITY_URL,
HEADERS_WITH_AUTH,
)
from iec_api.models.account import Account
from iec_api.models.account import decoder as account_decoder
from iec_api.models.address import City, Street
from iec_api.models.address import get_cities_decoder as cities_decoder
from iec_api.models.address import get_city_streets_decoder as streets_decoder
from iec_api.models.contract import Contract, Contracts
from iec_api.models.contract import decoder as contract_decoder
from iec_api.models.contract_check import ContractCheck
Expand All @@ -50,6 +57,8 @@
from iec_api.models.jwt import JWT
from iec_api.models.meter_reading import MeterReadings
from iec_api.models.meter_reading import decoder as meter_reading_decoder
from iec_api.models.outages import GetOutageByAddressRequest, GetOutageByAddressResponse
from iec_api.models.outages import decoder as outage_decoder
from iec_api.models.remote_reading import ReadingResolution, RemoteReadingRequest, RemoteReadingResponse
from iec_api.models.response_descriptor import ResponseWithDescriptor

Expand All @@ -58,7 +67,7 @@


async def _get_response_with_descriptor(
session: ClientSession, jwt_token: JWT, request_url: str, decoder: BasicDecoder[ResponseWithDescriptor[T]]
session: ClientSession, jwt_token: Optional[JWT], request_url: str, decoder: BasicDecoder[ResponseWithDescriptor[T]]
) -> T:
"""
A function to retrieve a response with a descriptor using a JWT token and a URL.
Expand All @@ -70,7 +79,9 @@ async def _get_response_with_descriptor(
Returns:
T: The response with a descriptor, with its type specified by the return type annotation.
"""
headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, jwt_token.id_token)
headers = (
commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, jwt_token.id_token) if jwt_token else session.headers
)
response = await commons.send_get_request(session=session, url=request_url, headers=headers)

response_with_descriptor = decoder.decode(response)
Expand All @@ -85,7 +96,7 @@ async def _get_response_with_descriptor(

async def _post_response_with_descriptor(
session: ClientSession,
jwt_token: JWT,
jwt_token: Optional[JWT],
request_url: str,
json_data: Optional[dict],
decoder: BasicDecoder[ResponseWithDescriptor[T]],
Expand All @@ -101,7 +112,9 @@ async def _post_response_with_descriptor(
Returns:
T: The response with a descriptor, with its type specified by the return type annotation.
"""
headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, jwt_token.id_token)
headers = (
commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, jwt_token.id_token) if jwt_token else session.headers
)
response = await commons.send_post_request(session=session, url=request_url, headers=headers, json_data=json_data)

response_with_descriptor = decoder.decode(response)
Expand Down Expand Up @@ -203,6 +216,80 @@ async def get_contract_check(session: ClientSession, token: JWT, contract_id: st
)


city_streets: dict[str, list[Street]] = {}
cities: list[City] = []


async def get_cities(session: ClientSession) -> Optional[list[City]]:
"""Get Cities response from IEC API."""

if len(cities) == 0:
response = await _get_response_with_descriptor(session, None, GET_CITIES_URL, cities_decoder)
if response:
cities.extend(response.data_collection)

return cities if cities else None


async def get_city(session: ClientSession, city_name: str) -> Optional[City]:
"""Get City by Name from cache or IEC API."""

all_cities = await get_cities(session)
return next((city for city in all_cities if all_cities and city.name == city_name), None)


async def get_city_streets(session: ClientSession, city: City | str | UUID) -> Optional[list[Street]]:
"""Get Cities response from cache or IEC API."""

if isinstance(city, str):
city_id = city
elif isinstance(city, UUID):
city_id = str(city)
else:
city_id = city.id

if city_id in city_streets:
return city_streets[city_id]

get_streets_response = await _get_response_with_descriptor(
session, jwt_token=None, request_url=GET_CITY_STREETS_URL.format(city_id=city_id), decoder=streets_decoder
)

if get_streets_response:
city_streets[city_id] = get_streets_response.data_collection
return city_streets[city_id]
else:
return None


async def get_city_street(session: ClientSession, city: City | str, street_name: str) -> Optional[Street]:
"""Get Cities response from IEC API."""

streets = await get_city_streets(session, city)
return next((street for street in streets if street.name == street_name), None)


async def get_outages(
session: ClientSession, token: JWT, city: City | str, street: Street | str, house_num: str
) -> Optional[GetOutageByAddressResponse]:
"""Get Cities response from IEC API."""

if isinstance(city, str):
city_id = city
else:
city_id = city.id

if isinstance(street, str):
street_id = street
else:
street_id = street.id

req = GetOutageByAddressRequest(city_code=city_id, house_code=street_id, house_number=house_num)
return await _post_response_with_descriptor(
session, token, GET_OUTAGES_BY_ADDRESS_URL, req.to_dict(), outage_decoder
)


async def get_last_meter_reading(
session: ClientSession, token: JWT, bp_number: str, contract_id: str
) -> Optional[MeterReadings]:
Expand Down
53 changes: 53 additions & 0 deletions iec_api/iec_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from aiohttp import ClientSession

from iec_api import commons, data, login
from iec_api.models.address import City, Street
from iec_api.models.contract import Contract
from iec_api.models.contract_check import ContractCheck
from iec_api.models.customer import Account, Customer
Expand All @@ -21,6 +22,7 @@
from iec_api.models.invoice import GetInvoicesBody
from iec_api.models.jwt import JWT
from iec_api.models.meter_reading import MeterReadings
from iec_api.models.outages import GetOutageByAddressResponse
from iec_api.models.remote_reading import ReadingResolution, RemoteReadingResponse

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -442,6 +444,57 @@ async def get_efs_messages(

return await data.get_efs_messages(self._session, self._token, contract_id, service_code)

async def get_all_cities(self) -> Optional[list[City]]:
"""
Get all cities
Returns:
list[City]: List of cities
"""
return await data.get_cities(self._session)

async def get_city(self, city_name: str) -> Optional[City]:
"""
Get all cities
Returns:
City: the relevant city
"""
return await data.get_city(self._session, city_name)

async def get_all_city_streets(self, city: City | str) -> Optional[list[Street]]:
"""
Get all city Streets
Args:
city (City): The city
Returns:
list[Street]: List of streets
"""
return await data.get_city_streets(self._session, city)

async def get_city_street_by_name(self, city: City | str, street_name: str) -> Optional[Street]:
"""
Get City Street
Args:
city (City | str): The city or city id
street_name (str): The street name
Returns:
City: the relevant city
"""
return await data.get_city_street(self._session, city, street_name)

async def get_outages_by_address(
self, city: City | str, street: Street | str, house_num: str
) -> Optional[GetOutageByAddressResponse]:
"""
Get City Street
Args:
city (City | str): The city or city id
street (Street | str): The street or street id
house_num (str): The house number
Returns:
City: the relevant city
"""
return await data.get_outages(self._session, self._token, city, street, house_num)

# ----------------
# Login/Token Flow
# ----------------
Expand Down
84 changes: 84 additions & 0 deletions iec_api/models/address.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from dataclasses import dataclass, field
from uuid import UUID

from mashumaro import DataClassDictMixin, field_options
from mashumaro.codecs import BasicDecoder

from iec_api.models.response_descriptor import ResponseWithDescriptor

# GET https://iecapi.iec.co.il//api/District/cities
#
# {
# "data": {
# "dataCollection": [
# {
# "name": "אלון תבור",
# "shovalCityCode": "2054",
# "logicalName": "iec_city",
# "id": "34698a40-28ec-ea11-a817-000d3a239ca0"
# },]},
# "reponseDescriptor": {
# "isSuccess": true,
# "code": "0",
# "description": ""
# }
# }
#
#
#
# GET https://iecapi.iec.co.il//api/District/streets/{city_id}
#
# {
# "data": {
# "streets": [
# {
# "name": "רח 2369",
# "shovalStreetCode": "100000759",
# "logicalName": "iec_street",
# "id": "d02b9dcc-9094-ea11-a811-000d3a228dfc"
# },
# "logicalName": "iec_city",
# "id": "a80a89b9-29e0-e911-a972-000d3a29fb7a"
# },
# "reponseDescriptor": {
# "isSuccess": true,
# "code": "0",
# "description": ""
# }
# }
#
#


@dataclass
class City(DataClassDictMixin):
name: str = field(metadata=field_options(alias="name"))
shoval_city_code: str = field(metadata=field_options(alias="shovalCityCode"))
logical_name: str = field(metadata=field_options(alias="logicalName"))
id: UUID = field(metadata=field_options(alias="id"))


@dataclass
class GetCitiesResponse(DataClassDictMixin):
data_collection: list[City] = field(metadata=field_options(alias="dataCollection"))


get_cities_decoder = BasicDecoder(ResponseWithDescriptor[GetCitiesResponse])


@dataclass
class Street(DataClassDictMixin):
name: str = field(metadata=field_options(alias="name"))
shoval_street_code: str = field(metadata=field_options(alias="shovalStreetCode"))
logical_name: str = field(metadata=field_options(alias="logicalName"))
id: UUID = field(metadata=field_options(alias="id"))


@dataclass
class GetCityStreetsResponse(DataClassDictMixin):
streets: list[Street] = field(metadata=field_options(alias="streets"))
logical_name: str = field(metadata=field_options(alias="logicalName"))
id: UUID = field(metadata=field_options(alias="id"))


get_city_streets_decoder = BasicDecoder(ResponseWithDescriptor[GetCityStreetsResponse])
4 changes: 2 additions & 2 deletions iec_api/models/contract_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ class ContractCheck(DataClassDictMixin):
house_number: str = field(metadata=field_options(alias="houseNumber"))
bp_type: str = field(metadata=field_options(alias="bpType"))
is_private: str = field(metadata=field_options(alias="isPrivate")) # "X" if private producer
has_direct_debit: bool = field(metadata=field_options(alias="hasDirectDebit"))
is_matam: bool = field(metadata=field_options(alias="isMatam"))
has_direct_debit: bool = field(metadata=field_options(alias="hasDirectDebit")) # הוראת קבע
is_matam: bool = field(metadata=field_options(alias="isMatam")) # Prepaid Meter (מונה תשלום מראש)
frequency: Optional[InvoiceFrequency] = field(metadata=field_options(alias="frequency"))


Expand Down
2 changes: 1 addition & 1 deletion iec_api/models/invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class Invoice(DataClassDictMixin):
invoice_payment_status: int = field(metadata=field_options(alias="invoicePaymentStatus"))
document_id: str = field(metadata=field_options(alias="documentID"))
days_period: str = field(metadata=field_options(alias="daysPeriod"))
has_direct_debit: bool = field(metadata=field_options(alias="hasDirectDebit"))
has_direct_debit: bool = field(metadata=field_options(alias="hasDirectDebit")) # הוראת קבע
invoice_type: int = field(metadata=field_options(alias="invoiceType"))

reading_code: int = field(metadata=field_options(alias="readingCode"), default=0)
Expand Down
Loading