WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit faad3f1

Browse files
authored
Merge pull request #120 from zweckj/copilot/fix-mashumaro-serialization-issue
Filter unknown WidgetType enum values to prevent API breakage
2 parents c351194 + 5be7162 commit faad3f1

File tree

4 files changed

+312
-12
lines changed

4 files changed

+312
-12
lines changed

pylamarzocco/models/_config.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import json
6+
import logging
57
from dataclasses import dataclass, field
68
from datetime import datetime, timezone
79
from typing import Any
@@ -24,17 +26,55 @@
2426
WidgetType,
2527
)
2628

29+
_LOGGER = logging.getLogger(__name__)
30+
2731
from ._general import (
2832
BaseWidget,
2933
BaseWidgetOutput,
3034
CommandResponse,
3135
Thing,
3236
Widget,
3337
)
34-
3538
from ._update import FirmwareSettings
3639

3740

41+
def _filter_valid_widgets(
42+
items: list[dict[str, Any] | str], field_name: str
43+
) -> list[dict[str, Any] | str]:
44+
"""Filter items with valid WidgetType codes, logging warnings for invalid ones.
45+
46+
Args:
47+
items: List of widget dicts or code strings
48+
field_name: Name of the field being filtered (for logging)
49+
50+
Returns:
51+
List of items with valid widget codes
52+
"""
53+
valid_items = []
54+
for item in items:
55+
# Extract code - either directly if string, or from 'code' key if dict
56+
if code := (item if isinstance(item, str) else item.get("code")):
57+
try:
58+
WidgetType(code)
59+
except ValueError:
60+
# Log entire JSON if it's a dict, otherwise just the code
61+
if isinstance(item, dict):
62+
_LOGGER.warning(
63+
"Unknown widget in field '%s' will be discarded: %s",
64+
field_name,
65+
json.dumps(item),
66+
)
67+
else:
68+
_LOGGER.warning(
69+
"Unknown widget code '%s' in field '%s' will be discarded",
70+
code,
71+
field_name,
72+
)
73+
else:
74+
valid_items.append(item)
75+
return valid_items
76+
77+
3878
@dataclass(kw_only=True)
3979
class ThingConfig(DataClassJSONMixin):
4080
"""Dashboard config with widgets."""
@@ -44,11 +84,16 @@ class ThingConfig(DataClassJSONMixin):
4484

4585
@classmethod
4686
def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
47-
# move code to widget_type for mashumaro annotated serialization
48-
widgets = d["widgets"]
49-
for widget in widgets:
50-
widget["output"]["widget_type"] = widget["code"]
51-
d["widgets"] = widgets
87+
# Filter out widgets with unknown codes and log warnings
88+
widgets = d.get("widgets", [])
89+
valid_widgets = _filter_valid_widgets(widgets, "widgets")
90+
91+
# Set widget_type for valid widgets
92+
for item in valid_widgets:
93+
assert isinstance(item, dict)
94+
item["output"]["widget_type"] = item["code"]
95+
96+
d["widgets"] = valid_widgets
5297
return d
5398

5499
@classmethod
@@ -75,6 +120,17 @@ class ThingDashboardWebsocketConfig(ThingConfig):
75120
uuid: str
76121
commands: list[CommandResponse]
77122

123+
@classmethod
124+
def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
125+
# First call parent's __pre_deserialize__ to handle widgets
126+
d = super().__pre_deserialize__(d)
127+
128+
# Filter out removed_widgets with unknown codes and log warnings
129+
d["removedWidgets"] = _filter_valid_widgets(
130+
d.get("removedWidgets", []), "removedWidgets"
131+
)
132+
return d
133+
78134

79135
@dataclass(kw_only=True)
80136
class MachineStatus(BaseWidgetOutput):

pylamarzocco/models/_statistics.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import logging
56
from dataclasses import dataclass, field
67
from datetime import datetime, timezone
78
from typing import Any
@@ -13,6 +14,47 @@
1314

1415
from ._general import BaseWidgetOutput, Thing, Widget
1516

17+
_LOGGER = logging.getLogger(__name__)
18+
19+
20+
def _filter_valid_widgets(
21+
items: list[dict[str, Any] | str], field_name: str
22+
) -> list[dict[str, Any] | str]:
23+
"""Filter items with valid WidgetType codes, logging warnings for invalid ones.
24+
25+
Args:
26+
items: List of widget dicts or code strings
27+
field_name: Name of the field being filtered (for logging)
28+
29+
Returns:
30+
List of items with valid widget codes
31+
"""
32+
valid_items = []
33+
for item in items:
34+
# Extract code - either directly if string, or from 'code' key if dict
35+
code = item if isinstance(item, str) else item.get("code")
36+
if code:
37+
try:
38+
WidgetType(code)
39+
except ValueError:
40+
# Log entire JSON if it's a dict, otherwise just the code
41+
if isinstance(item, dict):
42+
import json
43+
_LOGGER.warning(
44+
"Unknown widget in field '%s' will be discarded: %s",
45+
field_name,
46+
json.dumps(item),
47+
)
48+
else:
49+
_LOGGER.warning(
50+
"Unknown widget code '%s' in field '%s' will be discarded",
51+
code,
52+
field_name,
53+
)
54+
else:
55+
valid_items.append(item)
56+
return valid_items
57+
1658

1759
@dataclass(kw_only=True)
1860
class ThingStatistics(Thing):
@@ -44,11 +86,26 @@ class ThingStatistics(Thing):
4486

4587
@classmethod
4688
def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
47-
# move code to widget_type for mashumaro annotated serialization
48-
widgets = d["selectedWidgets"]
49-
for widget in widgets:
50-
widget["output"]["widget_type"] = widget["code"]
51-
d["selectedWidgets"] = widgets
89+
# Filter out widgets with unknown codes and log warnings
90+
valid_widgets = _filter_valid_widgets(
91+
d.get("selectedWidgets", []), "selectedWidgets"
92+
)
93+
94+
# Set widget_type for valid widgets
95+
for item in valid_widgets:
96+
assert isinstance(item, dict)
97+
item["output"]["widget_type"] = item["code"]
98+
99+
d["selectedWidgets"] = valid_widgets
100+
101+
# Filter widget code lists using helper
102+
d["selectedWidgetCodes"] = _filter_valid_widgets(
103+
d.get("selectedWidgetCodes", []), "selectedWidgetCodes"
104+
)
105+
d["allWidgetCodes"] = _filter_valid_widgets(
106+
d.get("allWidgetCodes", []), "allWidgetCodes"
107+
)
108+
52109
return d
53110

54111
@classmethod

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pylamarzocco"
3-
version = "2.1.2"
3+
version = "2.1.3"
44
license = { text = "MIT" }
55
description = "A Python implementation of the La Marzocco API"
66
readme = "README.md"

0 commit comments

Comments
 (0)