Skip to content

Commit 57cc381

Browse files
committed
Added optional validation to attributes that are considered mandatory in the spec.
1 parent 13a6b65 commit 57cc381

6 files changed

Lines changed: 85 additions & 12 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,5 @@ dmypy.json
136136

137137
# Windows python venv
138138
/Scripts/
139-
pyvenv.cfg
139+
pyvenv.cfg
140+
/share/

src/mpd_parser/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ class UnknownElementTreeParseError(Exception):
1818
class NoPeriodAncestorForTargetElement(Exception):
1919
""" Raised when trying to create a timeline from template without parent period """
2020
description = "targeted segment template is not nested under a periods"
21+
22+
class InvalidManifestMissingMandatoryElementError(Exception):
23+
""" Raised when the manifest is missing a mandatory element """
24+
description = "manifest is missing a mandatory element, check the MPD specification for more information"

src/mpd_parser/models/base_tags.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,22 @@ def __setattr__(self, key: str, value: Any) -> None:
4646

4747
self.element.attrib[element_attrib_name] = str(value)
4848

49+
def validate(self) -> None:
50+
"""
51+
Validate the tag, should be implemented by subclasses when adding @mandatory attributes,
52+
overload this method and use super to call it after validating specific elements.
53+
"""
54+
for attr_name in dir(self):
55+
# Skip private/protected and methods
56+
if attr_name.startswith("_"):
57+
continue
58+
attr = getattr(self, attr_name)
59+
# Check if it's a list of Tag instances
60+
if isinstance(attr, list) and attr and isinstance(attr[0], Tag):
61+
for child in attr:
62+
child.validate()
63+
64+
4965
@classmethod
5066
def to_camel_case(cls, snake_case_string: str) -> str:
5167
"""convert snake_case to lowerCamelCase"""

src/mpd_parser/models/composite_tags.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
)
3535
from mpd_parser.models.segment_tags import MultipleSegmentBase, SegmentBase, SegmentList
3636
from mpd_parser.timeline_utils import SegmentTiming
37+
from mpd_parser.validation import mandatory
3738

3839

3940
class Period(Tag):
@@ -156,6 +157,15 @@ def __init__(self, element: Element, encoding: str = "utf-8"):
156157
self.encoding = encoding
157158
self.tag_map = {"cenc": "xlmns:cenc"}
158159

160+
def validate(self):
161+
"""Validate the MPD object against the DASH specification.
162+
This starts a recursive validation of all the tags and their children.
163+
Raises:
164+
InvalidManifestMissingMandatoryElementError: if a mandatory element is missing
165+
"""
166+
_ = self.min_buffer_time
167+
super().validate()
168+
159169
@cached_property
160170
def namespace(self):
161171
value = self.element.nsmap
@@ -234,10 +244,19 @@ def minimum_update_period_in_seconds(self):
234244
else TWO_SECONDS # default for minimumUpdatePeriod
235245
)
236246

237-
@cached_property
247+
@mandatory
238248
def min_buffer_time(self):
239249
return self.element.attrib.get("minBufferTime")
240250

251+
@property
252+
@lru_cache
253+
def min_buffer_time_in_seconds(self) -> float:
254+
return (
255+
parse_duration(self.min_buffer_time).total_seconds()
256+
if self.min_buffer_time
257+
else ZERO_SECONDS
258+
)
259+
241260
@cached_property
242261
def time_shift_buffer_depth(self):
243262
return self.element.attrib.get("timeShiftBufferDepth")
@@ -551,11 +570,17 @@ def content_component(self):
551570
class Representation(RepresentationBase):
552571
"""Representation tag"""
553572

554-
@cached_property
573+
def validate(self):
574+
""" Check mandatory attributes have values. """
575+
_ = self.id
576+
_ = self.bandwidth
577+
super().validate()
578+
579+
@mandatory
555580
def id(self):
556581
return self.element.attrib.get("id")
557582

558-
@cached_property
583+
@mandatory
559584
def bandwidth(self):
560585
return get_int_value(self.element.attrib.get("bandwidth"))
561586

src/mpd_parser/parser.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Parser:
2828
"""
2929

3030
@classmethod
31-
def from_string(cls, manifest_as_string: str) -> MPD:
31+
def from_string(cls, manifest_as_string: str, validate: bool = False) -> MPD:
3232
"""generate a parsed mpd object from a given string
3333
3434
Args:
@@ -57,12 +57,15 @@ def cut_and_burn(match: Match) -> str:
5757
except Exception as err:
5858
logger.exception("Failed to parse manifest string")
5959
raise UnknownElementTreeParseError() from err
60-
if encoding:
61-
return MPD(root, encoding=encoding[0].groups()[0])
62-
return MPD(root)
60+
61+
mpd = MPD(root, encoding=encoding[0].groups()[0] if encoding else "utf-8")
62+
63+
if validate:
64+
mpd.validate()
65+
return mpd
6366

6467
@classmethod
65-
def from_file(cls, manifest_file_name: str) -> MPD:
68+
def from_file(cls, manifest_file_name: str, validate: bool = False) -> MPD:
6669
"""
6770
Generate a parsed mpd object from a given file name
6871
Args:
@@ -81,10 +84,14 @@ def from_file(cls, manifest_file_name: str) -> MPD:
8184
except Exception as err:
8285
logger.exception("Failed to parse manifest file %s", manifest_file_name)
8386
raise UnknownElementTreeParseError() from err
84-
return MPD(tree.getroot())
87+
88+
mpd = MPD(tree.getroot())
89+
if validate:
90+
mpd.validate()
91+
return mpd
8592

8693
@classmethod
87-
def from_url(cls, url: str) -> MPD:
94+
def from_url(cls, url: str, validate: bool = False) -> MPD:
8895
"""
8996
Generate a parsed mpd object from a given URL
9097
Args:
@@ -104,7 +111,11 @@ def from_url(cls, url: str) -> MPD:
104111
except Exception as err:
105112
logger.exception("Failed to parse manifest from URL %s", url)
106113
raise UnknownElementTreeParseError() from err
107-
return MPD(tree.getroot())
114+
115+
mpd = MPD(tree.getroot())
116+
if validate:
117+
mpd.validate()
118+
return mpd
108119

109120
@classmethod
110121
def to_string(cls, mpd: MPD) -> str:

src/mpd_parser/validation.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""
2+
This module provides validation tools for tags and attributes.
3+
"""
4+
from functools import cached_property
5+
from mpd_parser.exceptions import InvalidManifestMissingMandatoryElementError
6+
7+
def mandatory(func):
8+
""" Decorator to mark a method as mandatory. """
9+
@cached_property
10+
def wrapper(self, *args, **kwargs):
11+
value = func(self, *args, **kwargs)
12+
if value is None:
13+
raise InvalidManifestMissingMandatoryElementError(
14+
f"Mandatory attribute '{func.__name__}' is missing in {type(self).__name__}")
15+
return value
16+
return wrapper

0 commit comments

Comments
 (0)