From 2d42924f612dc37947d56e528a6e72eaed37d700 Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Thu, 11 Apr 2024 12:10:16 +0300 Subject: [PATCH 1/6] feat: Add GetCities, GetCityStreets, GetOutagesByAddress APIs --- iec_api/const.py | 4 ++ iec_api/data.py | 61 ++++++++++++++++++++++++++-- iec_api/models/address.py | 84 +++++++++++++++++++++++++++++++++++++++ iec_api/models/outages.py | 62 +++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 iec_api/models/address.py create mode 100644 iec_api/models/outages.py diff --git a/iec_api/const.py b/iec_api/const.py index c1bea9f..e79858e 100644 --- a/iec_api/const.py +++ b/iec_api/const.py @@ -41,5 +41,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" diff --git a/iec_api/data.py b/iec_api/data.py index 3f2c76b..cf0d409 100644 --- a/iec_api/data.py +++ b/iec_api/data.py @@ -11,6 +11,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, @@ -22,11 +24,15 @@ GET_INVOICE_PDF_URL, GET_KWH_TARIFF_URL, GET_LAST_METER_READING_URL, + GET_OUTAGES_BY_ADDRESS_URL, GET_REQUEST_READING_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 @@ -47,6 +53,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 @@ -55,7 +63,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. @@ -67,7 +75,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) @@ -82,7 +92,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]], @@ -98,7 +108,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) @@ -200,6 +212,47 @@ async def get_contract_check(session: ClientSession, token: JWT, contract_id: st ) +async def get_cities(session: ClientSession) -> Optional[list[City]]: + """Get Cities response from IEC API.""" + get_cities_response = await _get_response_with_descriptor(session, None, GET_CITIES_URL, cities_decoder) + + return get_cities_response.data_collection if get_cities_response else None + + +async def get_city(session: ClientSession, city_name: str) -> Optional[City]: + """Get City by Name from IEC API.""" + + cities = await get_cities(session) + return next((city for city in cities if city.name == city_name), None) + + +async def get_city_streets(session: ClientSession, city: City) -> Optional[list[Street]]: + """Get Cities response from IEC API.""" + get_streets_response = await _get_response_with_descriptor( + session, jwt_token=None, request_url=GET_CITY_STREETS_URL.format(city_id=str(city.id)), decoder=streets_decoder + ) + + return get_streets_response.streets if get_streets_response else None + + +async def get_city_street(session: ClientSession, city: City, 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, city: City, street: Street, house_num: str +) -> Optional[GetOutageByAddressResponse]: + """Get Cities response from IEC API.""" + + req = GetOutageByAddressRequest(city_code=city.id, house_code=street.id, logical_name=house_num) + return await _post_response_with_descriptor( + session, None, 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]: diff --git a/iec_api/models/address.py b/iec_api/models/address.py new file mode 100644 index 0000000..7b9fa01 --- /dev/null +++ b/iec_api/models/address.py @@ -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]) diff --git a/iec_api/models/outages.py b/iec_api/models/outages.py new file mode 100644 index 0000000..bf17e52 --- /dev/null +++ b/iec_api/models/outages.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass, field +from datetime import datetime +from typing import Optional +from uuid import UUID + +from mashumaro import DataClassDictMixin, field_options +from mashumaro.codecs import BasicDecoder +from mashumaro.config import BaseConfig + +from iec_api.models.response_descriptor import ResponseWithDescriptor + +# +# POST https://iecapi.iec.co.il//api/outages/transactionsByAddress +# BODY { +# "cityCode":"34698a40-28ec-ea11-a817-000d3a239ca0", +# "streetCode":"d02b9dcc-9094-ea11-a811-000d3a228dfc" +# "houseNumber":"2" +# } +# +# Response: +# { +# "data": { +# "readDataTime": "2024-04-08T06:41:57.1502783+00:00", +# "transaction": { +# "disconnectionType": "", +# "estimateTreatmentDate": null, +# "messageToDisplay": null +# } +# }, +# "reponseDescriptor": { +# "isSuccess": true, +# "code": "0", +# "description": "" +# } +# } +# + + +@dataclass +class GetOutageByAddressRequest(DataClassDictMixin): + city_code: UUID = field(metadata=field_options(alias="cityCode")) + house_code: UUID = field(metadata=field_options(alias="streetCode")) + logical_name: str = field(metadata=field_options(alias="houseNumber")) + + class Config(BaseConfig): + serialize_by_alias = True + + +@dataclass +class OutageTransaction(DataClassDictMixin): + disconnection_type: Optional[str] = field(metadata=field_options(alias="disconnectionType")) + estimate_treatment_date: Optional[datetime] = field(metadata=field_options(alias="estimateTreatmentDate")) + message_to_display: Optional[str] = field(metadata=field_options(alias="messageToDisplay")) + + +@dataclass +class GetOutageByAddressResponse(DataClassDictMixin): + read_data_time: datetime = field(metadata=field_options(alias="readDataTime")) + transaction: OutageTransaction = field(metadata=field_options(alias="transaction")) + + +decoder = BasicDecoder(ResponseWithDescriptor[GetOutageByAddressResponse]) From 8d2b8bbe22fb5027745ac3619709f0182e135a8b Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Thu, 11 Apr 2024 15:15:05 +0300 Subject: [PATCH 2/6] feat: Add changes to IecClient --- iec_api/data.py | 26 +++++++++++++++++---- iec_api/iec_client.py | 53 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/iec_api/data.py b/iec_api/data.py index cf0d409..0be655e 100644 --- a/iec_api/data.py +++ b/iec_api/data.py @@ -226,16 +226,22 @@ async def get_city(session: ClientSession, city_name: str) -> Optional[City]: return next((city for city in cities if city.name == city_name), None) -async def get_city_streets(session: ClientSession, city: City) -> Optional[list[Street]]: +async def get_city_streets(session: ClientSession, city: City | str) -> Optional[list[Street]]: """Get Cities response from IEC API.""" + + if isinstance(city, str): + city_id = city + else: + city_id = city.id + get_streets_response = await _get_response_with_descriptor( - session, jwt_token=None, request_url=GET_CITY_STREETS_URL.format(city_id=str(city.id)), decoder=streets_decoder + session, jwt_token=None, request_url=GET_CITY_STREETS_URL.format(city_id=city_id), decoder=streets_decoder ) return get_streets_response.streets if get_streets_response else None -async def get_city_street(session: ClientSession, city: City, street_name: str) -> Optional[Street]: +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) @@ -243,11 +249,21 @@ async def get_city_street(session: ClientSession, city: City, street_name: str) async def get_outages( - session: ClientSession, city: City, street: Street, house_num: str + session: ClientSession, city: City | str, street: Street | str, house_num: str ) -> Optional[GetOutageByAddressResponse]: """Get Cities response from IEC API.""" - req = GetOutageByAddressRequest(city_code=city.id, house_code=street.id, logical_name=house_num) + 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, logical_name=house_num) return await _post_response_with_descriptor( session, None, GET_OUTAGES_BY_ADDRESS_URL, req.to_dict(), outage_decoder ) diff --git a/iec_api/iec_client.py b/iec_api/iec_client.py index 0e37eb0..4943b43 100644 --- a/iec_api/iec_client.py +++ b/iec_api/iec_client.py @@ -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 @@ -20,6 +21,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__) @@ -412,6 +414,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, city, street, house_num) + # ---------------- # Login/Token Flow # ---------------- From 18c4b7b9d15c989c38926bf24f52a655cf3b5748 Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Thu, 11 Apr 2024 15:24:34 +0300 Subject: [PATCH 3/6] feat: Add to IECClient --- iec_api/data.py | 6 +++--- iec_api/iec_client.py | 2 +- iec_api/models/outages.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/iec_api/data.py b/iec_api/data.py index 0be655e..c29f557 100644 --- a/iec_api/data.py +++ b/iec_api/data.py @@ -249,7 +249,7 @@ async def get_city_street(session: ClientSession, city: City | str, street_name: async def get_outages( - session: ClientSession, city: City | str, street: Street | str, house_num: str + session: ClientSession, token: JWT, city: City | str, street: Street | str, house_num: str ) -> Optional[GetOutageByAddressResponse]: """Get Cities response from IEC API.""" @@ -263,9 +263,9 @@ async def get_outages( else: street_id = street.id - req = GetOutageByAddressRequest(city_code=city_id, house_code=street_id, logical_name=house_num) + req = GetOutageByAddressRequest(city_code=city_id, house_code=street_id, house_number=house_num) return await _post_response_with_descriptor( - session, None, GET_OUTAGES_BY_ADDRESS_URL, req.to_dict(), outage_decoder + session, token, GET_OUTAGES_BY_ADDRESS_URL, req.to_dict(), outage_decoder ) diff --git a/iec_api/iec_client.py b/iec_api/iec_client.py index 4943b43..1a21ae5 100644 --- a/iec_api/iec_client.py +++ b/iec_api/iec_client.py @@ -463,7 +463,7 @@ async def get_outages_by_address( Returns: City: the relevant city """ - return await data.get_outages(self._session, city, street, house_num) + return await data.get_outages(self._session, self._token, city, street, house_num) # ---------------- # Login/Token Flow diff --git a/iec_api/models/outages.py b/iec_api/models/outages.py index bf17e52..abeb457 100644 --- a/iec_api/models/outages.py +++ b/iec_api/models/outages.py @@ -40,7 +40,7 @@ class GetOutageByAddressRequest(DataClassDictMixin): city_code: UUID = field(metadata=field_options(alias="cityCode")) house_code: UUID = field(metadata=field_options(alias="streetCode")) - logical_name: str = field(metadata=field_options(alias="houseNumber")) + house_number: str = field(metadata=field_options(alias="houseNumber")) class Config(BaseConfig): serialize_by_alias = True From efed3083f3651ea05bcd056c42c325da6e8cc82c Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Sun, 14 Apr 2024 14:20:11 +0300 Subject: [PATCH 4/6] feat: add some field explanations --- iec_api/models/contract_check.py | 4 ++-- iec_api/models/invoice.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iec_api/models/contract_check.py b/iec_api/models/contract_check.py index 670276b..acee642 100644 --- a/iec_api/models/contract_check.py +++ b/iec_api/models/contract_check.py @@ -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")) diff --git a/iec_api/models/invoice.py b/iec_api/models/invoice.py index a96ca84..1f8687d 100644 --- a/iec_api/models/invoice.py +++ b/iec_api/models/invoice.py @@ -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) From 1ec800371316077b4c29a1a05dda9dbf22db447d Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Mon, 15 Apr 2024 11:34:38 +0300 Subject: [PATCH 5/6] feat: cahce city and city streets --- iec_api/data.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/iec_api/data.py b/iec_api/data.py index c29f557..c4dadfa 100644 --- a/iec_api/data.py +++ b/iec_api/data.py @@ -2,6 +2,7 @@ import logging from datetime import datetime from typing import Optional, TypeVar +from uuid import UUID from aiohttp import ClientSession from mashumaro.codecs import BasicDecoder @@ -212,33 +213,50 @@ 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.""" - get_cities_response = await _get_response_with_descriptor(session, None, GET_CITIES_URL, cities_decoder) - return get_cities_response.data_collection if get_cities_response else None + 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 IEC API.""" + """Get City by Name from cache or IEC API.""" cities = await get_cities(session) return next((city for city in cities if city.name == city_name), None) -async def get_city_streets(session: ClientSession, city: City | str) -> Optional[list[Street]]: - """Get Cities response from IEC API.""" +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 ) - return get_streets_response.streets if get_streets_response else None + 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]: From e958c8f5d293de4b7e4f341b91eaffe05bad53e0 Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky Date: Mon, 15 Apr 2024 11:53:24 +0300 Subject: [PATCH 6/6] fix: check None for get_city --- iec_api/data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iec_api/data.py b/iec_api/data.py index c4dadfa..e81d8ce 100644 --- a/iec_api/data.py +++ b/iec_api/data.py @@ -231,8 +231,8 @@ async def get_cities(session: ClientSession) -> Optional[list[City]]: async def get_city(session: ClientSession, city_name: str) -> Optional[City]: """Get City by Name from cache or IEC API.""" - cities = await get_cities(session) - return next((city for city in cities if city.name == city_name), None) + 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]]: