Skip to content

Commit c28ad1e

Browse files
authored
feat: add parser library
- clean up unused main - drop support for python 3.9 since it is end of life - implement a basic parser registry
1 parent 1a84dac commit c28ad1e

File tree

9 files changed

+190
-14
lines changed

9 files changed

+190
-14
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ jobs:
3636
fail-fast: false
3737
matrix:
3838
python-version:
39-
- "3.9"
4039
- "3.10"
4140
- "3.11"
4241
- "3.12"

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ license = { text = "Apache Software License 2.0" }
1111
authors = [
1212
{ name = "Open Video Libs", email = "openvideolibs@koston.org" },
1313
]
14-
requires-python = ">=3.9"
14+
requires-python = ">=3.10"
1515
classifiers = [
1616
"Development Status :: 2 - Pre-Alpha",
1717
"Intended Audience :: Developers",
1818
"Natural Language :: English",
1919
"Operating System :: OS Independent",
20-
"Programming Language :: Python :: 3.9",
2120
"Programming Language :: Python :: 3.10",
2221
"Programming Language :: Python :: 3.11",
2322
"Programming Language :: Python :: 3.12",
2423
"Programming Language :: Python :: 3.13",
24+
"Programming Language :: Python :: 3.14",
2525
"Topic :: Software Development :: Libraries",
2626
]
2727

@@ -35,6 +35,7 @@ urls.repository = "https://github.com/openvideolibs/onvif-parsers"
3535
[dependency-groups]
3636
dev = [
3737
"pytest>=8,<9",
38+
"pytest-asyncio>=1.2.0",
3839
"pytest-cov>=6,<8",
3940
]
4041
docs = [
@@ -88,6 +89,7 @@ addopts = """\
8889
--cov-report=xml
8990
"""
9091
pythonpath = [ "src" ]
92+
asyncio_default_fixture_loop_scope = "session"
9193

9294
[tool.coverage.run]
9395
branch = true

src/onvif_parsers/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1+
from . import registry
2+
13
__version__ = "0.0.1"
4+
5+
__all__ = ["get"]
6+
7+
get = registry.get_parser

src/onvif_parsers/main.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/onvif_parsers/model.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from dataclasses import dataclass
2+
from typing import Any
3+
4+
5+
@dataclass
6+
class EventEntity:
7+
"""Represents a ONVIF event entity."""
8+
9+
# Unique identifier for the entity
10+
uid: str
11+
# Human-readable name for the entity
12+
name: str
13+
# Type of platform (e.g., sensor, binary_sensor)
14+
platform: str
15+
# Optional device class (e.g., motion, alarm, safety). The options vary based on the
16+
# platform.
17+
# See https://www.home-assistant.io/integrations/homeassistant/#device-class
18+
device_class: str | None = None
19+
# Optional unit of measurement (e.g., percent)
20+
unit_of_measurement: str | None = None
21+
# Current value of the entity. Most onvif events are boolean (true/false), but this
22+
# could be an integer or timestamp or other data types supported as well.
23+
value: Any = None
24+
# Optional entity category (e.g., diagnostic, configuration). The default (sensor)
25+
# does not need to be specified.
26+
entity_category: str | None = None
27+
# Indicates whether the entity is enabled by default. Defaults to True.
28+
entity_enabled: bool = True

src/onvif_parsers/registry.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import typing
2+
from collections.abc import Callable
3+
4+
from . import model
5+
6+
# Type alias for a parser callable. It should be an async function.
7+
# Args:
8+
# str: The uid of the entity.
9+
# Any: The raw event data. zeep.xsd.ComplexType or zeep.xsd.AnySimpleType.
10+
# TODO: could we make this zeep.Type or zeep.AnyType?
11+
# Returns:
12+
# Awaitable[model.EventEntity]: The parsed EventEntity.
13+
ParserCallable: typing.TypeAlias = Callable[
14+
[str, typing.Any], typing.Awaitable[model.EventEntity | None]
15+
]
16+
17+
18+
class Registry:
19+
"""A registry of parsers."""
20+
21+
def __init__(self) -> None:
22+
self.registry: dict[str, ParserCallable] = {}
23+
24+
def register(self, key: str, f: ParserCallable) -> None:
25+
"""Register a parser function under a given key."""
26+
if key in self.registry:
27+
raise ValueError(f"Key {key} already registered")
28+
29+
self.registry[key] = f
30+
31+
def get(self, key: str) -> ParserCallable | None:
32+
"""Get a parser function by key."""
33+
return self.registry.get(key)
34+
35+
36+
_REGISTRY = Registry()
37+
38+
39+
def register(topic: str) -> Callable[[ParserCallable], ParserCallable]:
40+
"""Register an onvif parser callable with the given topic."""
41+
42+
def wrapper(func: ParserCallable) -> ParserCallable:
43+
_REGISTRY.register(topic, func)
44+
return func
45+
46+
return wrapper
47+
48+
49+
def get_parser(topic: str) -> ParserCallable | None:
50+
"""Get a parser callable for the given topic."""
51+
return _REGISTRY.get(topic)

tests/test_main.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

tests/test_registry.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import typing
2+
3+
import pytest
4+
5+
from onvif_parsers import model, registry
6+
7+
8+
@registry.register("decorator_test_topic")
9+
async def parser1(uid: str, _: typing.Any) -> model.EventEntity | None:
10+
return model.EventEntity(
11+
uid=uid,
12+
name="Test",
13+
platform="sensor",
14+
)
15+
16+
17+
def test_get_parser_none():
18+
"""Getting a non-registered parser returns None."""
19+
parser = registry.get_parser("non_existent_topic")
20+
assert parser is None
21+
22+
23+
@pytest.mark.asyncio
24+
async def test_registration_works():
25+
"""Registering a parser works as expected."""
26+
registry.register("test_topic1")(parser1)
27+
parser = registry.get_parser("test_topic1")
28+
assert parser is not None
29+
assert callable(parser)
30+
assert await parser("entity_1", None) == model.EventEntity(
31+
uid="entity_1", name="Test", platform="sensor"
32+
)
33+
34+
35+
@pytest.mark.asyncio
36+
async def test_decorator_registration():
37+
"""Registering a parser via decorator works as expected."""
38+
parser = registry.get_parser("decorator_test_topic")
39+
assert parser is not None
40+
assert callable(parser)
41+
assert await parser("entity_1", None) == model.EventEntity(
42+
uid="entity_1", name="Test", platform="sensor"
43+
)
44+
45+
46+
def test_double_registration():
47+
"""Registering the same topic twice raises ValueError."""
48+
registry.register("test_topic2")(parser1)
49+
50+
with pytest.raises(ValueError):
51+
registry.register("test_topic2")(parser1)

uv.lock

Lines changed: 50 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)