violet-poolController-api 0.0.37__tar.gz → 0.0.38__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {violet_poolcontroller_api-0.0.37/violet_poolController_api.egg-info → violet_poolcontroller_api-0.0.38}/PKG-INFO +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/pyproject.toml +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_api.py +1 -1
- violet_poolcontroller_api-0.0.38/tests/test_language_policy.py +136 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38/violet_poolController_api.egg-info}/PKG-INFO +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolController_api.egg-info/SOURCES.txt +1 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/__init__.py +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/api.py +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/circuit_breaker.py +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/const_api.py +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/const_devices.py +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/parsers.py +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/readings.py +1 -1
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/utils_rate_limiter.py +7 -7
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/utils_sanitizer.py +14 -14
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/LICENSE +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/README.md +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/setup.cfg +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_api_smoke.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_circuit_breaker.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_mock_server.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_parsers.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_rate_limiter.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_readings.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_sanitizer.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolController_api.egg-info/dependency_links.txt +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolController_api.egg-info/requires.txt +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolController_api.egg-info/top_level.txt +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/_api_dosing.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/_api_mixin.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/_api_model.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/_api_outputs.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/_api_readings.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/_api_system.py +0 -0
- {violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/violet_poolcontroller_api/py.typed +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# violet-poolController-api - API for Violet Pool Controller
|
|
2
|
+
# Copyright (C) 2024-2026 Xerolux
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
# by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU Affero General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
"""Everything written into this repository is English.
|
|
18
|
+
|
|
19
|
+
The policy lives in AGENTS.md; this file is what makes it hold. Two things are
|
|
20
|
+
deliberately exempt and must stay exempt: the controller's own error strings in
|
|
21
|
+
``const_api.py`` (and the German payloads the mock server replays), which are
|
|
22
|
+
data rather than prose, and the German half of the bilingual documentation
|
|
23
|
+
under ``docs/de/``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# ruff: noqa: S101
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
import pytest
|
|
34
|
+
|
|
35
|
+
REPO = Path(__file__).parent.parent
|
|
36
|
+
PACKAGE = REPO / "violet_poolcontroller_api"
|
|
37
|
+
|
|
38
|
+
# Text the controller emits, reproduced verbatim. Translating it would break
|
|
39
|
+
# the match with what the device actually says.
|
|
40
|
+
#
|
|
41
|
+
# Matched by file name, not by path: `python -m build` copies the package into
|
|
42
|
+
# `build/lib/`, and an exemption keyed to the original path does not cover the
|
|
43
|
+
# copy. That is not hypothetical - it failed the 0.0.38 release job, because
|
|
44
|
+
# the release builds the wheel before running the checks.
|
|
45
|
+
CONTROLLER_VERBATIM = frozenset({"const_api.py", "mock_server.py"})
|
|
46
|
+
|
|
47
|
+
# This file has to name the German words it looks for.
|
|
48
|
+
SELF = Path(__file__).name
|
|
49
|
+
|
|
50
|
+
# Directories that hold generated or vendored copies of the sources. Scanning
|
|
51
|
+
# them says nothing about what is written in this repository.
|
|
52
|
+
GENERATED_DIRS = frozenset({".git", ".tox", ".venv", "venv", "build", "dist", "__pycache__"})
|
|
53
|
+
|
|
54
|
+
# Words that only appear in German prose. Deliberately not "in", "die" or "der":
|
|
55
|
+
# those collide with English or with identifiers.
|
|
56
|
+
GERMAN_WORDS = (
|
|
57
|
+
"für",
|
|
58
|
+
"über",
|
|
59
|
+
"nicht",
|
|
60
|
+
"wird",
|
|
61
|
+
"werden",
|
|
62
|
+
"wenn",
|
|
63
|
+
"diese",
|
|
64
|
+
"dieser",
|
|
65
|
+
"keine",
|
|
66
|
+
"sollte",
|
|
67
|
+
)
|
|
68
|
+
_GERMAN = re.compile(r"\b(" + "|".join(GERMAN_WORDS) + r")\b", re.IGNORECASE)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _python_sources() -> list[Path]:
|
|
72
|
+
"""Return every Python file the policy applies to."""
|
|
73
|
+
return sorted(
|
|
74
|
+
path
|
|
75
|
+
for path in REPO.rglob("*.py")
|
|
76
|
+
if not (GENERATED_DIRS & set(path.parts))
|
|
77
|
+
and not any(part.endswith(".egg-info") for part in path.parts)
|
|
78
|
+
and path.name not in CONTROLLER_VERBATIM
|
|
79
|
+
and path.name != SELF
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize("path", _python_sources(), ids=lambda p: p.name)
|
|
84
|
+
def test_python_sources_are_english(path: Path) -> None:
|
|
85
|
+
"""A German comment is read by people who do not speak German."""
|
|
86
|
+
offenders = [
|
|
87
|
+
f"{path.relative_to(REPO)}:{number}: {line.strip()}"
|
|
88
|
+
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1)
|
|
89
|
+
if _GERMAN.search(line)
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
assert not offenders, "German text outside the exempt files:\n" + "\n".join(offenders)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_the_changelog_is_english() -> None:
|
|
96
|
+
"""The changelog is what a consumer reads when a version breaks something.
|
|
97
|
+
|
|
98
|
+
Code spans are quotations, not prose: an entry that removes a German string
|
|
99
|
+
has to be able to name the string it removed.
|
|
100
|
+
"""
|
|
101
|
+
changelog = (REPO / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
102
|
+
prose = re.sub(r"`[^`]*`", "", changelog)
|
|
103
|
+
|
|
104
|
+
offenders = sorted({match.group(0).lower() for match in _GERMAN.finditer(prose)})
|
|
105
|
+
|
|
106
|
+
assert not offenders, f"German outside code spans in CHANGELOG.md: {offenders}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_the_controller_strings_stay_german() -> None:
|
|
110
|
+
"""The exemption is the point, not an oversight - guard it too.
|
|
111
|
+
|
|
112
|
+
If someone "cleans up" the error table into English, the messages stop
|
|
113
|
+
matching what the device reports.
|
|
114
|
+
"""
|
|
115
|
+
const_api = (PACKAGE / "const_api.py").read_text(encoding="utf-8")
|
|
116
|
+
|
|
117
|
+
assert "Filterdrucküberwachung (Druck zu niedrig)" in const_api
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_generated_copies_are_not_scanned() -> None:
|
|
121
|
+
"""A built wheel is not source, and scanning its copy fails the release.
|
|
122
|
+
|
|
123
|
+
`python -m build` writes `build/lib/<package>/`, so the release job runs
|
|
124
|
+
the checks with a duplicate of every module on disk.
|
|
125
|
+
"""
|
|
126
|
+
scanned = {path.name for path in _python_sources()}
|
|
127
|
+
|
|
128
|
+
assert "const_api.py" not in scanned
|
|
129
|
+
assert not [path for path in _python_sources() if "build" in path.parts]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_the_policy_is_written_down() -> None:
|
|
133
|
+
"""Without the rule in AGENTS.md this file is just an opinion."""
|
|
134
|
+
agents = (REPO / "AGENTS.md").read_text(encoding="utf-8")
|
|
135
|
+
|
|
136
|
+
assert "## Language Policy" in agents
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# violet-poolController-api - API
|
|
1
|
+
# violet-poolController-api - API for Violet Pool Controller
|
|
2
2
|
# Copyright (C) 2024-2026 Xerolux
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# You should have received a copy of the GNU Affero General Public License
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
|
-
"""Rate
|
|
17
|
+
"""Rate limiter for API requests - token bucket algorithm."""
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
@@ -34,8 +34,8 @@ class RateLimiter:
|
|
|
34
34
|
|
|
35
35
|
Verhindert API-Overload durch:
|
|
36
36
|
- Maximale Requests pro Zeitfenster
|
|
37
|
-
- Burst
|
|
38
|
-
- Priority
|
|
37
|
+
- Burst support for short spikes
|
|
38
|
+
- Priority queue for critical requests
|
|
39
39
|
- Graceful Degradation bei Limit-Überschreitung
|
|
40
40
|
"""
|
|
41
41
|
|
|
@@ -89,7 +89,7 @@ class RateLimiter:
|
|
|
89
89
|
"last_minute_reset": time.monotonic(),
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
# Lock
|
|
92
|
+
# Lock for thread safety
|
|
93
93
|
self._lock = asyncio.Lock()
|
|
94
94
|
self._waiters: list[tuple[int, int, asyncio.Event]] = []
|
|
95
95
|
self._waiter_sequence = 0
|
|
@@ -169,7 +169,7 @@ class RateLimiter:
|
|
|
169
169
|
timeout: Maximale Wartezeit in Sekunden
|
|
170
170
|
|
|
171
171
|
Raises:
|
|
172
|
-
TimeoutError:
|
|
172
|
+
TimeoutError: If the timeout is reached
|
|
173
173
|
|
|
174
174
|
"""
|
|
175
175
|
start_time = time.monotonic()
|
|
@@ -279,7 +279,7 @@ class RateLimiter:
|
|
|
279
279
|
_LOGGER.debug("Rate Limiter zurückgesetzt")
|
|
280
280
|
|
|
281
281
|
|
|
282
|
-
# Global
|
|
282
|
+
# Global rate limiter instance (one can also be created per API instance)
|
|
283
283
|
_global_rate_limiter: RateLimiter | None = None
|
|
284
284
|
|
|
285
285
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# violet-poolController-api - API
|
|
1
|
+
# violet-poolController-api - API for Violet Pool Controller
|
|
2
2
|
# Copyright (C) 2024-2026 Xerolux
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
"""Input
|
|
18
|
+
"""Input sanitization utilities for user inputs and API parameters."""
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
@@ -33,11 +33,11 @@ _MAX_API_PARAM_LENGTH = 100
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class InputSanitizer:
|
|
36
|
-
"""Input
|
|
36
|
+
"""Input sanitization for security and data integrity.
|
|
37
37
|
|
|
38
38
|
Schützt vor:
|
|
39
39
|
- XSS (Cross-Site Scripting)
|
|
40
|
-
- SQL
|
|
40
|
+
- SQL injection (not relevant for an HTTP API, but defended against)
|
|
41
41
|
- Command Injection
|
|
42
42
|
- Path Traversal
|
|
43
43
|
- Unerwarteten Zeichen
|
|
@@ -85,14 +85,14 @@ class InputSanitizer:
|
|
|
85
85
|
# Konvertiere zu String
|
|
86
86
|
str_value = str(value).strip()
|
|
87
87
|
|
|
88
|
-
# Unicode
|
|
88
|
+
# Unicode normalisation (NFKD, defence in depth)
|
|
89
89
|
# Normalized Form Compatibility Decomposition
|
|
90
90
|
str_value = unicodedata.normalize("NFKD", str_value)
|
|
91
91
|
|
|
92
92
|
# Längen-Validierung
|
|
93
93
|
if len(str_value) > max_length:
|
|
94
94
|
_LOGGER.warning(
|
|
95
|
-
"String
|
|
95
|
+
"String too long (%d > %d), truncating: %s...",
|
|
96
96
|
len(str_value),
|
|
97
97
|
max_length,
|
|
98
98
|
str_value[:50],
|
|
@@ -233,7 +233,7 @@ class InputSanitizer:
|
|
|
233
233
|
float_value = float(value)
|
|
234
234
|
if not math.isfinite(float_value):
|
|
235
235
|
_LOGGER.warning(
|
|
236
|
-
"
|
|
236
|
+
"Non-finite float value '%s', using default %.2f",
|
|
237
237
|
value,
|
|
238
238
|
default,
|
|
239
239
|
)
|
|
@@ -315,7 +315,7 @@ class InputSanitizer:
|
|
|
315
315
|
|
|
316
316
|
"""
|
|
317
317
|
if not key:
|
|
318
|
-
msg = "Device
|
|
318
|
+
msg = "Device key must not be empty"
|
|
319
319
|
raise ValueError(msg)
|
|
320
320
|
|
|
321
321
|
# Nur Großbuchstaben, Zahlen und Underscore erlaubt
|
|
@@ -351,7 +351,7 @@ class InputSanitizer:
|
|
|
351
351
|
|
|
352
352
|
"""
|
|
353
353
|
if not param:
|
|
354
|
-
msg = "API
|
|
354
|
+
msg = "API parameter must not be empty"
|
|
355
355
|
raise ValueError(msg)
|
|
356
356
|
|
|
357
357
|
# Prüfe auf Path Traversal VOR der Bereinigung
|
|
@@ -458,27 +458,27 @@ class InputSanitizer:
|
|
|
458
458
|
)
|
|
459
459
|
|
|
460
460
|
|
|
461
|
-
# Singleton
|
|
461
|
+
# Singleton instance for convenient access
|
|
462
462
|
_sanitizer = InputSanitizer()
|
|
463
463
|
|
|
464
464
|
|
|
465
465
|
def sanitize_string(*args: Any, **kwargs: Any) -> str: # noqa: ANN401
|
|
466
|
-
"""Shortcut
|
|
466
|
+
"""Shortcut for InputSanitizer.sanitize_string()."""
|
|
467
467
|
return _sanitizer.sanitize_string(*args, **kwargs)
|
|
468
468
|
|
|
469
469
|
|
|
470
470
|
def sanitize_integer(*args: Any, **kwargs: Any) -> int: # noqa: ANN401
|
|
471
|
-
"""Shortcut
|
|
471
|
+
"""Shortcut for InputSanitizer.sanitize_integer()."""
|
|
472
472
|
return _sanitizer.sanitize_integer(*args, **kwargs)
|
|
473
473
|
|
|
474
474
|
|
|
475
475
|
def sanitize_float(*args: Any, **kwargs: Any) -> float: # noqa: ANN401
|
|
476
|
-
"""Shortcut
|
|
476
|
+
"""Shortcut for InputSanitizer.sanitize_float()."""
|
|
477
477
|
return _sanitizer.sanitize_float(*args, **kwargs)
|
|
478
478
|
|
|
479
479
|
|
|
480
480
|
def sanitize_boolean(*args: Any, **kwargs: Any) -> bool: # noqa: ANN401
|
|
481
|
-
"""Shortcut
|
|
481
|
+
"""Shortcut for InputSanitizer.sanitize_boolean()."""
|
|
482
482
|
return _sanitizer.sanitize_boolean(*args, **kwargs)
|
|
483
483
|
|
|
484
484
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_api_smoke.py
RENAMED
|
File without changes
|
{violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_circuit_breaker.py
RENAMED
|
File without changes
|
{violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_mock_server.py
RENAMED
|
File without changes
|
|
File without changes
|
{violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_rate_limiter.py
RENAMED
|
File without changes
|
{violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_readings.py
RENAMED
|
File without changes
|
{violet_poolcontroller_api-0.0.37 → violet_poolcontroller_api-0.0.38}/tests/test_sanitizer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|