arthexis 0.1.10__py3-none-any.whl → 0.1.12__py3-none-any.whl

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.

Potentially problematic release.


This version of arthexis might be problematic. Click here for more details.

Files changed (54) hide show
  1. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/METADATA +36 -26
  2. arthexis-0.1.12.dist-info/RECORD +102 -0
  3. config/context_processors.py +1 -0
  4. config/settings.py +31 -5
  5. config/urls.py +5 -4
  6. core/admin.py +430 -90
  7. core/apps.py +48 -2
  8. core/backends.py +38 -0
  9. core/environment.py +23 -5
  10. core/mailer.py +3 -1
  11. core/models.py +303 -31
  12. core/reference_utils.py +20 -9
  13. core/release.py +4 -0
  14. core/sigil_builder.py +7 -2
  15. core/sigil_resolver.py +35 -4
  16. core/system.py +250 -1
  17. core/tasks.py +92 -40
  18. core/temp_passwords.py +181 -0
  19. core/test_system_info.py +62 -2
  20. core/tests.py +169 -3
  21. core/user_data.py +51 -8
  22. core/views.py +371 -20
  23. nodes/admin.py +453 -8
  24. nodes/backends.py +21 -6
  25. nodes/dns.py +203 -0
  26. nodes/feature_checks.py +133 -0
  27. nodes/models.py +374 -31
  28. nodes/reports.py +411 -0
  29. nodes/tests.py +677 -38
  30. nodes/utils.py +32 -0
  31. nodes/views.py +14 -0
  32. ocpp/admin.py +278 -15
  33. ocpp/consumers.py +517 -16
  34. ocpp/evcs_discovery.py +158 -0
  35. ocpp/models.py +237 -4
  36. ocpp/reference_utils.py +42 -0
  37. ocpp/simulator.py +321 -22
  38. ocpp/store.py +110 -2
  39. ocpp/test_rfid.py +169 -7
  40. ocpp/tests.py +819 -6
  41. ocpp/transactions_io.py +17 -3
  42. ocpp/views.py +233 -19
  43. pages/admin.py +144 -4
  44. pages/context_processors.py +21 -7
  45. pages/defaults.py +13 -0
  46. pages/forms.py +38 -0
  47. pages/models.py +189 -15
  48. pages/tests.py +281 -8
  49. pages/urls.py +4 -0
  50. pages/views.py +137 -21
  51. arthexis-0.1.10.dist-info/RECORD +0 -95
  52. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/WHEEL +0 -0
  53. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/licenses/LICENSE +0 -0
  54. {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/top_level.txt +0 -0
nodes/dns.py ADDED
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from dataclasses import dataclass
5
+ from typing import Iterable, Mapping, MutableMapping, TYPE_CHECKING
6
+
7
+ import requests
8
+ from django.utils import timezone
9
+ from dns import exception as dns_exception
10
+ from dns import resolver as dns_resolver
11
+ from requests import Response
12
+
13
+ if TYPE_CHECKING: # pragma: no cover - imported for type checking only
14
+ from .models import DNSRecord, NodeManager
15
+
16
+
17
+ @dataclass
18
+ class DeploymentResult:
19
+ deployed: list["DNSRecord"]
20
+ failures: MutableMapping["DNSRecord", str]
21
+ skipped: MutableMapping["DNSRecord", str]
22
+
23
+
24
+ def _error_from_response(response: Response) -> str:
25
+ try:
26
+ payload = response.json()
27
+ except ValueError:
28
+ payload = None
29
+
30
+ if isinstance(payload, dict):
31
+ for key in ("message", "detail", "error"):
32
+ message = payload.get(key)
33
+ if message:
34
+ return str(message)
35
+ elif isinstance(payload, list) and payload:
36
+ first = payload[0]
37
+ if isinstance(first, Mapping) and "message" in first:
38
+ return str(first["message"])
39
+ return str(first)
40
+
41
+ reason = response.reason or ""
42
+ return f"{response.status_code} {reason}".strip()
43
+
44
+
45
+ def deploy_records(manager: "NodeManager", records: Iterable["DNSRecord"]) -> DeploymentResult:
46
+ filtered: list["DNSRecord"] = []
47
+ skipped: MutableMapping["DNSRecord", str] = {}
48
+ for record in records:
49
+ if record.provider != record.Provider.GODADDY:
50
+ skipped[record] = "Unsupported DNS provider"
51
+ continue
52
+ domain = record.get_domain(manager)
53
+ if not domain:
54
+ skipped[record] = "Domain is required for deployment"
55
+ continue
56
+ filtered.append(record)
57
+
58
+ if not filtered:
59
+ return DeploymentResult([], {}, skipped)
60
+
61
+ session = requests.Session()
62
+ session.headers.update(
63
+ {
64
+ "Authorization": manager.get_auth_header(),
65
+ "Accept": "application/json",
66
+ "Content-Type": "application/json",
67
+ }
68
+ )
69
+ customer_id = manager.get_customer_id()
70
+ if customer_id:
71
+ session.headers["X-Shopper-Id"] = customer_id
72
+
73
+ grouped: MutableMapping[tuple[str, str, str], list["DNSRecord"]] = defaultdict(list)
74
+ for record in filtered:
75
+ key = (
76
+ record.get_domain(manager),
77
+ record.record_type,
78
+ record.get_name(),
79
+ )
80
+ grouped[key].append(record)
81
+
82
+ deployed: list["DNSRecord"] = []
83
+ failures: MutableMapping["DNSRecord", str] = {}
84
+ now = timezone.now()
85
+
86
+ base_url = manager.get_base_url()
87
+ for (domain, record_type, name), grouped_records in grouped.items():
88
+ payload = [record.to_godaddy_payload() for record in grouped_records]
89
+ url = f"{base_url}/v1/domains/{domain}/records/{record_type}/{name or '@'}"
90
+ try:
91
+ response = session.put(url, json=payload, timeout=30)
92
+ except requests.RequestException as exc:
93
+ message = str(exc)
94
+ for record in grouped_records:
95
+ record.mark_error(message, manager=manager)
96
+ failures[record] = message
97
+ continue
98
+
99
+ if response.status_code >= 400:
100
+ message = _error_from_response(response)
101
+ for record in grouped_records:
102
+ record.mark_error(message, manager=manager)
103
+ failures[record] = message
104
+ continue
105
+
106
+ for record in grouped_records:
107
+ record.mark_deployed(manager=manager, timestamp=now)
108
+ deployed.append(record)
109
+
110
+ return DeploymentResult(deployed, failures, skipped)
111
+
112
+
113
+ def create_resolver() -> dns_resolver.Resolver:
114
+ return dns_resolver.Resolver()
115
+
116
+
117
+ def _normalize_hostname(value: str) -> str:
118
+ return value.rstrip(".").lower()
119
+
120
+
121
+ def _extract_txt(rdata) -> str:
122
+ strings = getattr(rdata, "strings", None)
123
+ if strings:
124
+ parts = []
125
+ for segment in strings:
126
+ if isinstance(segment, bytes):
127
+ parts.append(segment.decode("utf-8", "ignore"))
128
+ else:
129
+ parts.append(str(segment))
130
+ return "".join(parts)
131
+ return str(rdata).strip('"')
132
+
133
+
134
+ def _matches_record(record: "DNSRecord", rdata) -> bool:
135
+ expected = (record.resolve_sigils("data") or "").strip()
136
+ rtype = record.record_type
137
+
138
+ if rtype == record.Type.A:
139
+ return getattr(rdata, "address", str(rdata)) == expected
140
+ if rtype == record.Type.AAAA:
141
+ return getattr(rdata, "address", str(rdata)) == expected
142
+ if rtype == record.Type.CNAME:
143
+ actual = getattr(rdata, "target", None)
144
+ if actual is None:
145
+ return False
146
+ return _normalize_hostname(actual.to_text()) == _normalize_hostname(expected)
147
+ if rtype == record.Type.NS:
148
+ actual = getattr(rdata, "target", None)
149
+ if actual is None:
150
+ return False
151
+ return _normalize_hostname(actual.to_text()) == _normalize_hostname(expected)
152
+ if rtype == record.Type.MX:
153
+ host = getattr(rdata, "exchange", None)
154
+ if host is None:
155
+ return False
156
+ host_match = _normalize_hostname(host.to_text()) == _normalize_hostname(expected)
157
+ priority = getattr(rdata, "preference", None)
158
+ priority_match = record.priority is None or priority == record.priority
159
+ return host_match and priority_match
160
+ if rtype == record.Type.SRV:
161
+ target = getattr(rdata, "target", None)
162
+ if target is None:
163
+ return False
164
+ target_match = _normalize_hostname(target.to_text()) == _normalize_hostname(expected)
165
+ priority_match = record.priority is None or getattr(rdata, "priority", None) == record.priority
166
+ weight_match = record.weight is None or getattr(rdata, "weight", None) == record.weight
167
+ port_match = record.port is None or getattr(rdata, "port", None) == record.port
168
+ return target_match and priority_match and weight_match and port_match
169
+ if rtype == record.Type.TXT:
170
+ actual = _extract_txt(rdata)
171
+ return actual == expected
172
+
173
+ return str(rdata).strip() == expected
174
+
175
+
176
+ def validate_record(
177
+ record: "DNSRecord", resolver: dns_resolver.Resolver | None = None
178
+ ) -> tuple[bool, str]:
179
+ resolver = resolver or create_resolver()
180
+ fqdn = record.fqdn()
181
+ if not fqdn:
182
+ message = "Domain is required for validation"
183
+ record.mark_error(message)
184
+ return False, message
185
+
186
+ try:
187
+ answers = resolver.resolve(fqdn, record.record_type)
188
+ except (dns_exception.DNSException, OSError) as exc:
189
+ message = str(exc)
190
+ record.mark_error(message)
191
+ return False, message
192
+
193
+ for rdata in answers:
194
+ if _matches_record(record, rdata):
195
+ record.last_verified_at = timezone.now()
196
+ record.last_error = ""
197
+ record.save(update_fields=["last_verified_at", "last_error"])
198
+ return True, ""
199
+
200
+ message = "DNS record does not match expected value"
201
+ record.mark_error(message)
202
+ return False, message
203
+
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable, Dict, Iterable, Optional
5
+
6
+ from django.contrib import messages
7
+
8
+ if False: # pragma: no cover - typing imports only
9
+ from .models import Node, NodeFeature
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class FeatureCheckResult:
14
+ """Outcome of a feature validation."""
15
+
16
+ success: bool
17
+ message: str
18
+ level: int = messages.INFO
19
+
20
+
21
+ FeatureCheck = Callable[["NodeFeature", Optional["Node"]], Any]
22
+
23
+
24
+ class FeatureCheckRegistry:
25
+ """Registry for feature validation callbacks."""
26
+
27
+ def __init__(self) -> None:
28
+ self._checks: Dict[str, FeatureCheck] = {}
29
+ self._default_check: Optional[FeatureCheck] = None
30
+
31
+ def register(self, slug: str) -> Callable[[FeatureCheck], FeatureCheck]:
32
+ """Register ``func`` as the validator for ``slug``."""
33
+
34
+ def decorator(func: FeatureCheck) -> FeatureCheck:
35
+ self._checks[slug] = func
36
+ return func
37
+
38
+ return decorator
39
+
40
+ def register_default(self, func: FeatureCheck) -> FeatureCheck:
41
+ """Register ``func`` as the fallback validator."""
42
+
43
+ self._default_check = func
44
+ return func
45
+
46
+ def get(self, slug: str) -> Optional[FeatureCheck]:
47
+ return self._checks.get(slug)
48
+
49
+ def items(self) -> Iterable[tuple[str, FeatureCheck]]:
50
+ return self._checks.items()
51
+
52
+ def run(
53
+ self, feature: "NodeFeature", *, node: Optional["Node"] = None
54
+ ) -> Optional[FeatureCheckResult]:
55
+ check = self._checks.get(feature.slug)
56
+ if check is None:
57
+ check = self._default_check
58
+ if check is None:
59
+ return None
60
+ result = check(feature, node)
61
+ return self._normalize_result(feature, result)
62
+
63
+ def _normalize_result(
64
+ self, feature: "NodeFeature", result: Any
65
+ ) -> FeatureCheckResult:
66
+ if isinstance(result, FeatureCheckResult):
67
+ return result
68
+ if result is None:
69
+ return FeatureCheckResult(
70
+ True,
71
+ f"{feature.display} check completed successfully.",
72
+ messages.SUCCESS,
73
+ )
74
+ if isinstance(result, tuple) and len(result) >= 2:
75
+ success, message, *rest = result
76
+ level = rest[0] if rest else (
77
+ messages.SUCCESS if success else messages.ERROR
78
+ )
79
+ return FeatureCheckResult(bool(success), str(message), int(level))
80
+ if isinstance(result, bool):
81
+ message = (
82
+ f"{feature.display} check {'passed' if result else 'failed'}."
83
+ )
84
+ level = messages.SUCCESS if result else messages.ERROR
85
+ return FeatureCheckResult(result, message, level)
86
+ raise TypeError(
87
+ f"Unsupported feature check result type: {type(result)!r}"
88
+ )
89
+
90
+
91
+ feature_checks = FeatureCheckRegistry()
92
+
93
+
94
+ @feature_checks.register_default
95
+ def _default_feature_check(
96
+ feature: "NodeFeature", node: Optional["Node"]
97
+ ) -> FeatureCheckResult:
98
+ from .models import Node
99
+
100
+ target: Optional["Node"] = node or Node.get_local()
101
+ if target is None:
102
+ return FeatureCheckResult(
103
+ False,
104
+ f"No local node is registered; cannot verify {feature.display}.",
105
+ messages.WARNING,
106
+ )
107
+ try:
108
+ enabled = feature.is_enabled
109
+ except Exception as exc: # pragma: no cover - defensive
110
+ return FeatureCheckResult(
111
+ False,
112
+ f"{feature.display} check failed: {exc}",
113
+ messages.ERROR,
114
+ )
115
+ if enabled:
116
+ return FeatureCheckResult(
117
+ True,
118
+ f"{feature.display} is enabled on {target.hostname}.",
119
+ messages.SUCCESS,
120
+ )
121
+ return FeatureCheckResult(
122
+ False,
123
+ f"{feature.display} is not enabled on {target.hostname}.",
124
+ messages.WARNING,
125
+ )
126
+
127
+
128
+ __all__ = [
129
+ "FeatureCheck",
130
+ "FeatureCheckRegistry",
131
+ "FeatureCheckResult",
132
+ "feature_checks",
133
+ ]