arthexis 0.1.16__py3-none-any.whl → 0.1.28__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.
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/METADATA +95 -41
- arthexis-0.1.28.dist-info/RECORD +112 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +21 -30
- config/settings_helpers.py +176 -1
- config/urls.py +69 -1
- core/admin.py +805 -473
- core/apps.py +6 -8
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/celery_utils.py +73 -0
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1825 -218
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/release.py +55 -7
- core/sigil_builder.py +2 -2
- core/sigil_resolver.py +1 -66
- core/system.py +285 -4
- core/tasks.py +439 -138
- core/test_system_info.py +43 -5
- core/tests.py +516 -18
- core/user_data.py +94 -21
- core/views.py +348 -186
- nodes/admin.py +904 -67
- nodes/apps.py +12 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +800 -127
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +98 -3
- nodes/tests.py +1381 -152
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1382 -152
- ocpp/admin.py +1970 -152
- ocpp/consumers.py +839 -34
- ocpp/models.py +968 -17
- ocpp/network.py +398 -0
- ocpp/store.py +411 -43
- ocpp/tasks.py +261 -3
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +194 -6
- ocpp/tests.py +1918 -87
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +8 -3
- ocpp/views.py +700 -53
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +28 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +86 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +1050 -126
- pages/urls.py +14 -2
- pages/utils.py +70 -0
- pages/views.py +622 -56
- arthexis-0.1.16.dist-info/RECORD +0 -111
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
config/settings_helpers.py
CHANGED
|
@@ -5,8 +5,12 @@ from __future__ import annotations
|
|
|
5
5
|
import contextlib
|
|
6
6
|
import ipaddress
|
|
7
7
|
import os
|
|
8
|
+
import socket
|
|
9
|
+
import subprocess
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
8
12
|
from pathlib import Path
|
|
9
|
-
from typing import Mapping, MutableMapping
|
|
13
|
+
from typing import Iterable, Mapping, MutableMapping
|
|
10
14
|
|
|
11
15
|
from django.core.management.utils import get_random_secret_key
|
|
12
16
|
from django.http import request as http_request
|
|
@@ -14,6 +18,7 @@ from django.http.request import split_domain_port
|
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
__all__ = [
|
|
21
|
+
"discover_local_ip_addresses",
|
|
17
22
|
"extract_ip_from_host",
|
|
18
23
|
"install_validate_host_with_subnets",
|
|
19
24
|
"load_secret_key",
|
|
@@ -78,6 +83,176 @@ def install_validate_host_with_subnets() -> None:
|
|
|
78
83
|
http_request.validate_host = _patched
|
|
79
84
|
|
|
80
85
|
|
|
86
|
+
def _normalize_candidate_ip(candidate: str) -> str | None:
|
|
87
|
+
"""Return a normalized IP string when *candidate* is valid."""
|
|
88
|
+
|
|
89
|
+
if not candidate:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
normalized = strip_ipv6_brackets(candidate.strip())
|
|
93
|
+
if not normalized:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Drop IPv6 zone identifiers (for example ``fe80::1%eth0``)
|
|
97
|
+
if "%" in normalized:
|
|
98
|
+
normalized = normalized.split("%", 1)[0]
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
return ipaddress.ip_address(normalized).compressed
|
|
102
|
+
except ValueError:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _iter_command_addresses(command: Iterable[str]) -> Iterable[str]:
|
|
107
|
+
"""Yield IP addresses parsed from a command's stdout."""
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
list(command),
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
check=False,
|
|
115
|
+
timeout=1.0,
|
|
116
|
+
)
|
|
117
|
+
except (FileNotFoundError, PermissionError, subprocess.SubprocessError):
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if result.returncode != 0:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
for token in result.stdout.split():
|
|
124
|
+
normalized = _normalize_candidate_ip(token)
|
|
125
|
+
if normalized:
|
|
126
|
+
yield normalized
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _iter_ip_addr_show() -> Iterable[str]:
|
|
130
|
+
"""Yield interface addresses from the ``ip`` command when available."""
|
|
131
|
+
|
|
132
|
+
commands = (
|
|
133
|
+
("ip", "-o", "-4", "addr", "show"),
|
|
134
|
+
("ip", "-o", "-6", "addr", "show"),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
for command in commands:
|
|
138
|
+
try:
|
|
139
|
+
result = subprocess.run(
|
|
140
|
+
list(command),
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
check=False,
|
|
144
|
+
timeout=1.0,
|
|
145
|
+
)
|
|
146
|
+
except (FileNotFoundError, PermissionError, subprocess.SubprocessError):
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if result.returncode != 0:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
for line in result.stdout.splitlines():
|
|
153
|
+
parts = line.split()
|
|
154
|
+
if len(parts) < 4:
|
|
155
|
+
continue
|
|
156
|
+
# ``<ifindex>: <ifname> <family> <address>/<prefix>``
|
|
157
|
+
address = parts[3].split("/", 1)[0]
|
|
158
|
+
normalized = _normalize_candidate_ip(address)
|
|
159
|
+
if normalized:
|
|
160
|
+
yield normalized
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _iter_metadata_addresses(env: Mapping[str, str]) -> Iterable[str]:
|
|
164
|
+
"""Yield IP addresses exposed by cloud metadata endpoints when available."""
|
|
165
|
+
|
|
166
|
+
disable_env = env.get("DISABLE_METADATA_IP_DISCOVERY", "")
|
|
167
|
+
if disable_env.strip().lower() in {"1", "true", "yes", "on"}:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if env.get("AWS_EC2_METADATA_DISABLED", "").strip().lower() in {"1", "true", "yes", "on"}:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
endpoints = (
|
|
174
|
+
"http://169.254.169.254/latest/meta-data/local-ipv4",
|
|
175
|
+
"http://169.254.169.254/latest/meta-data/public-ipv4",
|
|
176
|
+
"http://169.254.169.254/latest/meta-data/local-ipv6",
|
|
177
|
+
"http://169.254.169.254/latest/meta-data/ipv6",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
for endpoint in endpoints:
|
|
181
|
+
try:
|
|
182
|
+
with urllib.request.urlopen(endpoint, timeout=0.5) as response:
|
|
183
|
+
payload = response.read().decode("utf-8", "ignore").strip()
|
|
184
|
+
except (urllib.error.URLError, OSError, ValueError):
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
if not payload:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
for line in payload.splitlines():
|
|
191
|
+
normalized = _normalize_candidate_ip(line)
|
|
192
|
+
if normalized:
|
|
193
|
+
yield normalized
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def discover_local_ip_addresses(
|
|
197
|
+
env: Mapping[str, str] | MutableMapping[str, str] | None = None,
|
|
198
|
+
) -> set[str]:
|
|
199
|
+
"""Return IP addresses associated with the current host.
|
|
200
|
+
|
|
201
|
+
The discovery process aggregates several lightweight heuristics so the
|
|
202
|
+
project continues to run even when specific mechanisms fail. All
|
|
203
|
+
collectors are best-effort and errors are swallowed to avoid blocking
|
|
204
|
+
Django's startup.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
if env is None:
|
|
208
|
+
env = os.environ
|
|
209
|
+
|
|
210
|
+
addresses: set[str] = set()
|
|
211
|
+
|
|
212
|
+
def _add(candidate: str | None) -> None:
|
|
213
|
+
normalized = _normalize_candidate_ip(candidate or "")
|
|
214
|
+
if normalized:
|
|
215
|
+
addresses.add(normalized)
|
|
216
|
+
|
|
217
|
+
for loopback in ("127.0.0.1", "::1"):
|
|
218
|
+
_add(loopback)
|
|
219
|
+
|
|
220
|
+
hostnames: list[str] = []
|
|
221
|
+
with contextlib.suppress(Exception):
|
|
222
|
+
hostnames.append(socket.gethostname())
|
|
223
|
+
with contextlib.suppress(Exception):
|
|
224
|
+
hostnames.append(socket.getfqdn())
|
|
225
|
+
|
|
226
|
+
for hostname in hostnames:
|
|
227
|
+
if not hostname:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
with contextlib.suppress(Exception):
|
|
231
|
+
_hostname, _aliases, addresses_list = socket.gethostbyname_ex(hostname)
|
|
232
|
+
for address in addresses_list:
|
|
233
|
+
_add(address)
|
|
234
|
+
|
|
235
|
+
with contextlib.suppress(Exception):
|
|
236
|
+
for info in socket.getaddrinfo(hostname, None):
|
|
237
|
+
if len(info) < 5:
|
|
238
|
+
continue
|
|
239
|
+
sock_address = info[4]
|
|
240
|
+
if not sock_address:
|
|
241
|
+
continue
|
|
242
|
+
_add(sock_address[0])
|
|
243
|
+
|
|
244
|
+
for address in _iter_ip_addr_show():
|
|
245
|
+
_add(address)
|
|
246
|
+
|
|
247
|
+
for address in _iter_command_addresses(("hostname", "-I")):
|
|
248
|
+
_add(address)
|
|
249
|
+
|
|
250
|
+
for address in _iter_metadata_addresses(env):
|
|
251
|
+
_add(address)
|
|
252
|
+
|
|
253
|
+
return addresses
|
|
254
|
+
|
|
255
|
+
|
|
81
256
|
def load_secret_key(
|
|
82
257
|
base_dir: Path,
|
|
83
258
|
env: Mapping[str, str] | MutableMapping[str, str] | None = None,
|
config/urls.py
CHANGED
|
@@ -14,12 +14,15 @@ from django.conf import settings
|
|
|
14
14
|
from django.conf.urls.static import static
|
|
15
15
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
|
16
16
|
from django.contrib import admin
|
|
17
|
+
from django.core.exceptions import AppRegistryNotReady, ImproperlyConfigured
|
|
18
|
+
from django.db.utils import DatabaseError, OperationalError, ProgrammingError
|
|
17
19
|
from django.urls import include, path
|
|
18
20
|
import teams.admin # noqa: F401
|
|
19
21
|
from django.views.decorators.csrf import csrf_exempt
|
|
20
22
|
from django.views.generic import RedirectView
|
|
21
23
|
from django.views.i18n import set_language
|
|
22
24
|
from django.utils.translation import gettext_lazy as _
|
|
25
|
+
from django.http import Http404
|
|
23
26
|
from core import views as core_views
|
|
24
27
|
from core.admindocs import (
|
|
25
28
|
CommandsView,
|
|
@@ -28,6 +31,44 @@ from core.admindocs import (
|
|
|
28
31
|
)
|
|
29
32
|
from pages import views as pages_views
|
|
30
33
|
|
|
34
|
+
try: # Gate optional GraphQL dependency for roles that do not install it
|
|
35
|
+
from api.views import EnergyGraphQLView
|
|
36
|
+
except ModuleNotFoundError as exc: # pragma: no cover - dependency intentionally optional
|
|
37
|
+
if exc.name in {"graphene_django", "graphene"}:
|
|
38
|
+
EnergyGraphQLView = None # type: ignore[assignment]
|
|
39
|
+
else: # pragma: no cover - unrelated import error
|
|
40
|
+
raise
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _graphql_feature_enabled() -> bool:
|
|
44
|
+
"""Return ``True`` when the GraphQL endpoint should be exposed."""
|
|
45
|
+
|
|
46
|
+
if EnergyGraphQLView is None:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
from nodes.models import Node, NodeFeature
|
|
51
|
+
except (ModuleNotFoundError, AppRegistryNotReady, ImproperlyConfigured):
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
feature = NodeFeature.objects.filter(slug="graphql").first()
|
|
56
|
+
except (DatabaseError, OperationalError, ProgrammingError):
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
if feature is None:
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
node = Node.get_local()
|
|
64
|
+
except (DatabaseError, OperationalError, ProgrammingError):
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
if node and not node.has_feature("graphql"):
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
return True
|
|
71
|
+
|
|
31
72
|
admin.site.site_header = _("Constellation")
|
|
32
73
|
admin.site.site_title = _("Constellation")
|
|
33
74
|
|
|
@@ -134,6 +175,11 @@ urlpatterns = [
|
|
|
134
175
|
core_views.todo_done,
|
|
135
176
|
name="todo-done",
|
|
136
177
|
),
|
|
178
|
+
path(
|
|
179
|
+
"admin/core/todos/<int:pk>/delete/",
|
|
180
|
+
core_views.todo_delete,
|
|
181
|
+
name="todo-delete",
|
|
182
|
+
),
|
|
137
183
|
path(
|
|
138
184
|
"admin/core/todos/<int:pk>/snapshot/",
|
|
139
185
|
core_views.todo_snapshot,
|
|
@@ -149,12 +195,34 @@ urlpatterns = [
|
|
|
149
195
|
core_views.odoo_quote_report,
|
|
150
196
|
name="odoo-quote-report",
|
|
151
197
|
),
|
|
198
|
+
path(
|
|
199
|
+
"admin/request-temp-password/",
|
|
200
|
+
core_views.request_temp_password,
|
|
201
|
+
name="admin-request-temp-password",
|
|
202
|
+
),
|
|
152
203
|
path("admin/", admin.site.urls),
|
|
153
204
|
path("i18n/setlang/", csrf_exempt(set_language), name="set_language"),
|
|
154
|
-
path("api/", include("core.workgroup_urls")),
|
|
155
205
|
path("", include("pages.urls")),
|
|
156
206
|
]
|
|
157
207
|
|
|
208
|
+
_GRAPHQL_URLPOSITION = len(urlpatterns)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if EnergyGraphQLView is not None:
|
|
212
|
+
|
|
213
|
+
class FeatureFlaggedGraphQLView(EnergyGraphQLView):
|
|
214
|
+
"""GraphQL endpoint guarded by the node feature flag."""
|
|
215
|
+
|
|
216
|
+
def dispatch(self, request, *args, **kwargs): # type: ignore[override]
|
|
217
|
+
if not _graphql_feature_enabled():
|
|
218
|
+
raise Http404()
|
|
219
|
+
return super().dispatch(request, *args, **kwargs)
|
|
220
|
+
|
|
221
|
+
urlpatterns.insert(
|
|
222
|
+
_GRAPHQL_URLPOSITION,
|
|
223
|
+
path("graphql/", FeatureFlaggedGraphQLView.as_view(), name="graphql"),
|
|
224
|
+
)
|
|
225
|
+
|
|
158
226
|
urlpatterns += autodiscovered_urlpatterns()
|
|
159
227
|
|
|
160
228
|
if settings.DEBUG:
|