From 4331d024ffc94919e9afee09295414b97cd9b476 Mon Sep 17 00:00:00 2001 From: Layf <93056040+Layf21@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:23:16 +0100 Subject: [PATCH] feat: Added the raw attribute to dataclasses This new attribute stores the raw json of the api response for this object. --- .github/workflows/ci.yml | 1 - example.py | 3 +++ src/froeling/datamodels/component.py | 11 ++++++++++- src/froeling/datamodels/facility.py | 4 +++- src/froeling/datamodels/generics.py | 11 +++++++---- src/froeling/datamodels/notifications.py | 7 +++++-- src/froeling/datamodels/userdata.py | 4 +++- tests/test_components.py | 9 ++++++--- tests/test_facility.py | 17 ++++++++++++++++- tests/test_login.py | 5 ++++- tests/test_notifications.py | 5 +++++ 11 files changed, 62 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73a790e..6a80ea3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,4 +35,3 @@ jobs: run: hatch test -a - name: Test build run: hatch build - diff --git a/example.py b/example.py index f0eec3f..acb9e53 100644 --- a/example.py +++ b/example.py @@ -39,6 +39,9 @@ async def main(): facility = (await client.get_facilities())[0] # Get a list of all facilities print(facility) + # You can see the raw json data corresponding to a datamodel with the .raw attribute. + print(facility.raw) + # Get all components of the facility example_component = (await facility.get_components())[0] print(example_component) diff --git a/src/froeling/datamodels/component.py b/src/froeling/datamodels/component.py index bbb6b11..55b6273 100644 --- a/src/froeling/datamodels/component.py +++ b/src/froeling/datamodels/component.py @@ -1,6 +1,6 @@ """Represents Components and their Parameters.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from http import HTTPStatus from typing import Any @@ -34,6 +34,7 @@ class Component: time_windows_view (list[TimeWindowDay] | None): Time window data, if fetched. picture_url (str | None): URL to a representative image of the component. parameters (list[Parameter]): List of associated parameters. + raw (dict) """ @@ -49,6 +50,8 @@ class Component: parameters: dict[str, 'Parameter'] + raw: dict + def __init__(self, facility_id: int, component_id: str, session: Session): """Initialize a Component with minimal identifying information.""" self.facility_id = facility_id @@ -58,6 +61,7 @@ def __init__(self, facility_id: int, component_id: str, session: Session): self.time_windows_view = None self.picture_url = None self.parameters = {} + self.raw = {} @classmethod def _from_overview_data(cls, facility_id: int, session: Session, obj: dict) -> 'Component | None': @@ -72,6 +76,7 @@ def _from_overview_data(cls, facility_id: int, session: Session, obj: dict) -> ' component.standard_name = obj.get('standardName') component.type = obj.get('type') component.sub_type = obj.get('subType') + component.raw = obj return component def __str__(self) -> str: @@ -84,6 +89,7 @@ async def update(self) -> dict[str, 'Parameter']: 'get', endpoints.COMPONENT.format(self._session.user_id, self.facility_id, self.component_id), ) + self.raw = res self.component_id = res.get('componentId') # This should not be able to change. self.display_name = res.get('displayName') self.display_category = res.get('displayCategory') @@ -132,6 +138,8 @@ class Parameter: max_val: str | None string_list_key_values: dict[str, str] | None + raw: dict = field(repr=False, default_factory=dict) + @classmethod def _from_dict(cls, obj: dict, session: Session, facility_id: int) -> 'Parameter': parameter_id = obj['id'] @@ -158,6 +166,7 @@ def _from_dict(cls, obj: dict, session: Session, facility_id: int) -> 'Parameter min_val, max_val, string_list_key_values, + obj, ) @classmethod diff --git a/src/froeling/datamodels/facility.py b/src/froeling/datamodels/facility.py index 124103c..b2983e2 100644 --- a/src/froeling/datamodels/facility.py +++ b/src/froeling/datamodels/facility.py @@ -1,6 +1,6 @@ """Dataclasses relating to Facilities.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from froeling import endpoints from froeling.datamodels.component import Component @@ -28,6 +28,7 @@ class Facility: hours_since_last_maintenance: int | None operation_hours: int | None facility_generation: str | None + raw: dict = field(repr=False, default_factory=dict) @staticmethod def _from_dict(obj: dict, session: Session) -> 'Facility': @@ -84,6 +85,7 @@ def _from_dict(obj: dict, session: Session) -> 'Facility': hours_since_last_maintenance, operation_hours, facility_generation, + obj, ) @staticmethod diff --git a/src/froeling/datamodels/generics.py b/src/froeling/datamodels/generics.py index 8bc1454..4b9f5f6 100644 --- a/src/froeling/datamodels/generics.py +++ b/src/froeling/datamodels/generics.py @@ -1,6 +1,6 @@ """Generic datamodels used in multiple places/endpoints.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from enum import Enum @@ -20,6 +20,7 @@ class Address: zip: int | None city: str | None country: str | None + raw: dict = field(repr=False, default_factory=dict) @staticmethod def _from_dict(obj: dict) -> 'Address': @@ -27,7 +28,7 @@ def _from_dict(obj: dict) -> 'Address': zipcode = obj.get('zip') city = obj.get('city') country = obj.get('country') - return Address(street, zipcode, city, country) + return Address(street, zipcode, city, country, obj) class Weekday(Enum): @@ -56,6 +57,7 @@ class TimeWindowDay: id: int weekday: Weekday phases: list['TimeWindowPhase'] + raw: dict = field(repr=False, default_factory=dict) @classmethod def _from_dict(cls, obj: dict) -> 'TimeWindowDay': @@ -63,7 +65,7 @@ def _from_dict(cls, obj: dict) -> 'TimeWindowDay': weekday = Weekday(obj['weekDay']) phases = TimeWindowPhase._from_list(obj['phases']) # noqa: SLF001 - return cls(_id, weekday, phases) + return cls(_id, weekday, phases, obj) @classmethod def _from_list(cls, obj: list) -> list['TimeWindowDay']: @@ -86,6 +88,7 @@ class TimeWindowPhase: start_minute: int end_hour: int end_minute: int + raw: dict = field(repr=False, default_factory=dict) @classmethod def _from_dict(cls, obj: dict) -> 'TimeWindowPhase': @@ -94,7 +97,7 @@ def _from_dict(cls, obj: dict) -> 'TimeWindowPhase': eh = obj['endHour'] em = obj['endMinute'] - return cls(sh, sm, eh, em) + return cls(sh, sm, eh, em, obj) @classmethod def _from_list(cls, obj: list) -> list['TimeWindowPhase']: diff --git a/src/froeling/datamodels/notifications.py b/src/froeling/datamodels/notifications.py index be5e5d6..7e7c5d8 100644 --- a/src/froeling/datamodels/notifications.py +++ b/src/froeling/datamodels/notifications.py @@ -1,7 +1,7 @@ """Datamodels to represent Notifications and related objects.""" import datetime -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -22,6 +22,7 @@ class NotificationOverview: """Known Values: "ERROR", "INFO", "WARNING", "ALARM" """ facility_id: int | None facility_name: str | None + raw: dict details: 'NotificationDetails' @@ -31,7 +32,7 @@ def __init__(self, data: dict, session: 'Session') -> None: self._set_data(data) def _set_data(self, data: dict) -> None: - self.data = data + self.raw = data self.id = data.get('id') self.subject = data.get('subject') @@ -90,6 +91,7 @@ class NotificationSubmissionState: type: str | None submitted_to: str | None submission_result: str | None + raw: dict = field(repr=False, default_factory=dict) @classmethod def _from_dict(cls, obj: dict) -> 'NotificationSubmissionState': @@ -106,6 +108,7 @@ def _from_dict(cls, obj: dict) -> 'NotificationSubmissionState': notification_type, submitted_to, submission_result, + obj, ) @classmethod diff --git a/src/froeling/datamodels/userdata.py b/src/froeling/datamodels/userdata.py index dea51aa..009e656 100644 --- a/src/froeling/datamodels/userdata.py +++ b/src/froeling/datamodels/userdata.py @@ -1,6 +1,6 @@ """Datamodels related to the user account.""" -from dataclasses import dataclass +from dataclasses import dataclass, field from froeling.datamodels.generics import Address @@ -20,6 +20,7 @@ class UserData: active: bool | None picture_url: str | None facility_count: int | None + raw: dict = field(repr=False, default_factory=dict) @staticmethod def _from_dict(obj: dict) -> 'UserData': @@ -49,4 +50,5 @@ def _from_dict(obj: dict) -> 'UserData': active, picture_url, facility_count, + obj, ) diff --git a/tests/test_components.py b/tests/test_components.py index f8d0330..589864c 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -23,9 +23,10 @@ async def test_facility_get_components(load_json): async with Froeling(token=token) as api: f = await api.get_facility(12345) - c = await f.get_components() - assert len(c) == 5 - c = c[0] + components = await f.get_components() + assert len(components) == 5 + c = components[0] + assert c.raw == component_list_data[0] assert c.component_id == '1_100' assert c.display_name == 'some display name' @@ -50,7 +51,9 @@ async def test_component_update(load_json): async with Froeling(token=token) as api: c = api.get_component(12345, '1_100') + assert c.raw == {} await c.update() + assert c.raw == component_data for p in c.parameters.values(): p.display_value # TODO: Add asserts diff --git a/tests/test_facility.py b/tests/test_facility.py index c10857b..d1ac475 100644 --- a/tests/test_facility.py +++ b/tests/test_facility.py @@ -20,6 +20,9 @@ async def test_get_facility(load_json): assert len(f) == 2 f1, f2 = f + assert f1.raw == facility_data[0] # order should be the same, but I wouldn't call it a requirement + assert f2.raw == facility_data[1] + assert f1.facility_id == 12345 assert f1.equipment_number == 100321123 assert f1.status == 'OK' @@ -82,6 +85,9 @@ async def test_get_facility_modified(load_json): f = await api.get_facilities() assert len(f) == 3 f1, f2, f3 = f + assert f1.raw == facility_data[0] + assert f2.raw == facility_data[1] + assert f3.raw == facility_data[2] assert f1.facility_id == 12345 assert f1.equipment_number is None @@ -218,6 +224,9 @@ async def test_facility_get_components(load_json, component_id, expected): comp = next(c for c in components if c.component_id == component_id) + api_data = next(d for d in component_list_data if d["componentId"] == component_id) + assert comp.raw == api_data + for field, value in expected.items(): assert getattr(comp, field) == value assert comp.time_windows_view is None @@ -247,6 +256,12 @@ async def test_facility_get_component(load_json): async with Froeling(token=token) as api: f = await api.get_facility(12345) + assert f.raw == facility_data[0] c = f.get_component('1_100') + assert c.raw == {} c2 = api.get_component(12345, '1_100') - assert c.component_id == c2.component_id + assert c2.raw == {} + await c.update() + assert c.raw == component_data + await c2.update() + assert c2.raw == component_data diff --git a/tests/test_login.py b/tests/test_login.py index a209107..7bd96ff 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -22,6 +22,8 @@ async def test_login_success(load_json): async with Froeling(username='joe', password='pwd') as api: userdata = await api.get_userdata() # should be cached. No new requests + assert userdata.raw == login_data # This includes the token. + assert api.token == token assert api.user_id == 1234 @@ -80,6 +82,7 @@ async def test_request_auto_reauth(load_json): auto_reauth=True, token_callback=mock_token_callback, ) as api: - await api.get_userdata() + d = await api.get_userdata() + assert d.raw == user_data mock_token_callback.assert_called_once_with(new_token) diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 21a4dfd..980bfde 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -46,6 +46,8 @@ async def test_get_notifications(load_json): notifications = await api.get_notifications() assert len(notifications) == 3 for i, n in enumerate(notifications): + assert n.raw == notification_list_data[i] + assert n.id == (i + 1) * 10000000 + 123456 assert n.subject == f'Subject {i + 1}' assert n.unread == (False, True, None)[i] @@ -93,6 +95,7 @@ async def test_get_notification_info(load_json): assert notification_details == notification_details_2 d = notification_details + assert d.raw == notification_data assert d.id == 10123456 assert d.subject == 'Subject 1' assert d.body == 'Title\r\ntext' @@ -108,6 +111,7 @@ async def test_get_notification_info(load_json): s1, s2 = d.notification_submission_state_dto assert isinstance(s1, NotificationSubmissionState) + assert s1.raw == notification_data["notificationSubmissionStateDto"][0] assert s1.id == 12345678 assert s1.recipient == 'joe@example.com' assert s1.type == 'EMAIL' @@ -115,6 +119,7 @@ async def test_get_notification_info(load_json): assert s1.submission_result == 'SUCCESS' assert isinstance(s2, NotificationSubmissionState) + assert s2.raw == notification_data["notificationSubmissionStateDto"][1] assert s2.id == 12345679 assert s2.recipient == 'sometoken' assert s2.type == 'TOKEN'