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.

Files changed (67) hide show
  1. {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/METADATA +95 -41
  2. arthexis-0.1.28.dist-info/RECORD +112 -0
  3. config/asgi.py +1 -15
  4. config/middleware.py +47 -1
  5. config/settings.py +21 -30
  6. config/settings_helpers.py +176 -1
  7. config/urls.py +69 -1
  8. core/admin.py +805 -473
  9. core/apps.py +6 -8
  10. core/auto_upgrade.py +19 -4
  11. core/backends.py +13 -3
  12. core/celery_utils.py +73 -0
  13. core/changelog.py +66 -5
  14. core/environment.py +4 -5
  15. core/models.py +1825 -218
  16. core/notifications.py +1 -1
  17. core/reference_utils.py +10 -11
  18. core/release.py +55 -7
  19. core/sigil_builder.py +2 -2
  20. core/sigil_resolver.py +1 -66
  21. core/system.py +285 -4
  22. core/tasks.py +439 -138
  23. core/test_system_info.py +43 -5
  24. core/tests.py +516 -18
  25. core/user_data.py +94 -21
  26. core/views.py +348 -186
  27. nodes/admin.py +904 -67
  28. nodes/apps.py +12 -1
  29. nodes/feature_checks.py +30 -0
  30. nodes/models.py +800 -127
  31. nodes/rfid_sync.py +1 -1
  32. nodes/tasks.py +98 -3
  33. nodes/tests.py +1381 -152
  34. nodes/urls.py +15 -1
  35. nodes/utils.py +51 -3
  36. nodes/views.py +1382 -152
  37. ocpp/admin.py +1970 -152
  38. ocpp/consumers.py +839 -34
  39. ocpp/models.py +968 -17
  40. ocpp/network.py +398 -0
  41. ocpp/store.py +411 -43
  42. ocpp/tasks.py +261 -3
  43. ocpp/test_export_import.py +1 -0
  44. ocpp/test_rfid.py +194 -6
  45. ocpp/tests.py +1918 -87
  46. ocpp/transactions_io.py +9 -1
  47. ocpp/urls.py +8 -3
  48. ocpp/views.py +700 -53
  49. pages/admin.py +262 -30
  50. pages/apps.py +35 -0
  51. pages/context_processors.py +28 -21
  52. pages/defaults.py +1 -1
  53. pages/forms.py +31 -8
  54. pages/middleware.py +6 -2
  55. pages/models.py +86 -2
  56. pages/module_defaults.py +5 -5
  57. pages/site_config.py +137 -0
  58. pages/tests.py +1050 -126
  59. pages/urls.py +14 -2
  60. pages/utils.py +70 -0
  61. pages/views.py +622 -56
  62. arthexis-0.1.16.dist-info/RECORD +0 -111
  63. core/workgroup_urls.py +0 -17
  64. core/workgroup_views.py +0 -94
  65. {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
  66. {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
  67. {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
@@ -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: