arthexis 0.1.9__py3-none-any.whl → 0.1.26__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 (112) hide show
  1. arthexis-0.1.26.dist-info/METADATA +272 -0
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +29 -29
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -25
  9. config/context_processors.py +67 -68
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +71 -25
  14. config/offline.py +49 -49
  15. config/settings.py +676 -492
  16. config/settings_helpers.py +109 -0
  17. config/urls.py +228 -159
  18. config/wsgi.py +17 -17
  19. core/admin.py +4052 -2066
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +192 -151
  22. core/apps.py +350 -223
  23. core/auto_upgrade.py +72 -0
  24. core/backends.py +311 -124
  25. core/changelog.py +403 -0
  26. core/entity.py +149 -133
  27. core/environment.py +60 -43
  28. core/fields.py +168 -75
  29. core/form_fields.py +75 -0
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +183 -172
  32. core/github_repos.py +72 -0
  33. core/lcd_screen.py +78 -78
  34. core/liveupdate.py +25 -25
  35. core/log_paths.py +114 -100
  36. core/mailer.py +89 -83
  37. core/middleware.py +91 -91
  38. core/models.py +5041 -2195
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +107 -0
  42. core/release.py +940 -346
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -131
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +250 -284
  47. core/system.py +1425 -230
  48. core/tasks.py +538 -199
  49. core/temp_passwords.py +181 -0
  50. core/test_system_info.py +202 -43
  51. core/tests.py +2673 -1069
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +681 -495
  55. core/views.py +2484 -789
  56. core/widgets.py +213 -51
  57. nodes/admin.py +2236 -445
  58. nodes/apps.py +98 -70
  59. nodes/backends.py +160 -53
  60. nodes/dns.py +203 -0
  61. nodes/feature_checks.py +133 -0
  62. nodes/lcd.py +165 -165
  63. nodes/models.py +2375 -870
  64. nodes/reports.py +411 -0
  65. nodes/rfid_sync.py +210 -0
  66. nodes/signals.py +18 -0
  67. nodes/tasks.py +141 -46
  68. nodes/tests.py +5045 -1489
  69. nodes/urls.py +29 -13
  70. nodes/utils.py +172 -73
  71. nodes/views.py +1768 -304
  72. ocpp/admin.py +1775 -481
  73. ocpp/apps.py +25 -25
  74. ocpp/consumers.py +1843 -630
  75. ocpp/evcs.py +844 -928
  76. ocpp/evcs_discovery.py +158 -0
  77. ocpp/models.py +1417 -640
  78. ocpp/network.py +398 -0
  79. ocpp/reference_utils.py +42 -0
  80. ocpp/routing.py +11 -9
  81. ocpp/simulator.py +745 -368
  82. ocpp/status_display.py +26 -0
  83. ocpp/store.py +603 -403
  84. ocpp/tasks.py +479 -31
  85. ocpp/test_export_import.py +131 -130
  86. ocpp/test_rfid.py +1072 -540
  87. ocpp/tests.py +5494 -2296
  88. ocpp/transactions_io.py +197 -165
  89. ocpp/urls.py +50 -50
  90. ocpp/views.py +2024 -912
  91. pages/admin.py +1123 -396
  92. pages/apps.py +45 -10
  93. pages/checks.py +40 -40
  94. pages/context_processors.py +151 -85
  95. pages/defaults.py +13 -0
  96. pages/forms.py +221 -0
  97. pages/middleware.py +213 -153
  98. pages/models.py +720 -252
  99. pages/module_defaults.py +156 -0
  100. pages/site_config.py +137 -0
  101. pages/tasks.py +74 -0
  102. pages/tests.py +4009 -1389
  103. pages/urls.py +38 -20
  104. pages/utils.py +93 -12
  105. pages/views.py +1736 -762
  106. arthexis-0.1.9.dist-info/METADATA +0 -168
  107. arthexis-0.1.9.dist-info/RECORD +0 -92
  108. core/workgroup_urls.py +0 -17
  109. core/workgroup_views.py +0 -94
  110. nodes/actions.py +0 -70
  111. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  112. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
core/log_paths.py CHANGED
@@ -1,100 +1,114 @@
1
- """Helpers for selecting writable log directories."""
2
-
3
- from __future__ import annotations
4
-
5
- from pathlib import Path
6
- import os
7
- import sys
8
-
9
-
10
- def _is_root() -> bool:
11
- if hasattr(os, "geteuid"):
12
- try:
13
- return os.geteuid() == 0
14
- except OSError: # pragma: no cover - defensive for unusual platforms
15
- return False
16
- return False
17
-
18
-
19
- def _state_home(base_home: Path) -> Path:
20
- state_home = os.environ.get("XDG_STATE_HOME")
21
- if state_home:
22
- return Path(state_home).expanduser()
23
- return base_home / ".local" / "state"
24
-
25
-
26
- def select_log_dir(base_dir: Path) -> Path:
27
- """Choose a writable log directory for the current process."""
28
-
29
- default = base_dir / "logs"
30
- env_override = os.environ.get("ARTHEXIS_LOG_DIR")
31
- is_root = _is_root()
32
- sudo_user = os.environ.get("SUDO_USER")
33
-
34
- candidates: list[Path] = []
35
- if env_override:
36
- candidates.append(Path(env_override).expanduser())
37
-
38
- if is_root:
39
- if not sudo_user or sudo_user == "root":
40
- candidates.append(default)
41
- candidates.append(Path("/var/log/arthexis"))
42
- candidates.append(Path("/tmp/arthexis/logs"))
43
- else:
44
- home = Path.home()
45
- state_home = _state_home(home)
46
- candidates.extend(
47
- [
48
- default,
49
- state_home / "arthexis" / "logs",
50
- home / ".arthexis" / "logs",
51
- Path("/tmp/arthexis/logs"),
52
- ]
53
- )
54
-
55
- seen: set[Path] = set()
56
- ordered_candidates: list[Path] = []
57
- for candidate in candidates:
58
- candidate = candidate.expanduser()
59
- if candidate not in seen:
60
- seen.add(candidate)
61
- ordered_candidates.append(candidate)
62
-
63
- attempted: list[Path] = []
64
- chosen: Path | None = None
65
- for candidate in ordered_candidates:
66
- attempted.append(candidate)
67
- try:
68
- candidate.mkdir(parents=True, exist_ok=True)
69
- except OSError:
70
- continue
71
- if os.access(candidate, os.W_OK | os.X_OK):
72
- chosen = candidate
73
- break
74
-
75
- if chosen is None:
76
- attempted_str = (
77
- ", ".join(str(path) for path in attempted) if attempted else "none"
78
- )
79
- raise RuntimeError(
80
- f"Unable to create a writable log directory. Tried: {attempted_str}"
81
- )
82
-
83
- if chosen != default:
84
- if (
85
- attempted
86
- and attempted[0] == default
87
- and not os.access(default, os.W_OK | os.X_OK)
88
- ):
89
- print(
90
- f"Log directory {default} is not writable; using {chosen}",
91
- file=sys.stderr,
92
- )
93
- elif is_root and sudo_user and sudo_user != "root" and not env_override:
94
- print(
95
- f"Running with elevated privileges; writing logs to {chosen}",
96
- file=sys.stderr,
97
- )
98
-
99
- os.environ["ARTHEXIS_LOG_DIR"] = str(chosen)
100
- return chosen
1
+ """Helpers for selecting writable log directories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import os
7
+ import sys
8
+ import tempfile
9
+
10
+
11
+ def _is_root() -> bool:
12
+ if hasattr(os, "geteuid"):
13
+ try:
14
+ return os.geteuid() == 0
15
+ except OSError: # pragma: no cover - defensive for unusual platforms
16
+ return False
17
+ return False
18
+
19
+
20
+ def _state_home(base_home: Path) -> Path:
21
+ state_home = os.environ.get("XDG_STATE_HOME")
22
+ if state_home:
23
+ return Path(state_home).expanduser()
24
+ return base_home / ".local" / "state"
25
+
26
+
27
+ def select_log_dir(base_dir: Path) -> Path:
28
+ """Choose a writable log directory for the current process."""
29
+
30
+ default = base_dir / "logs"
31
+ env_override = os.environ.get("ARTHEXIS_LOG_DIR")
32
+ is_root = _is_root()
33
+ sudo_user = os.environ.get("SUDO_USER")
34
+
35
+ candidates: list[Path] = []
36
+ if env_override:
37
+ candidates.append(Path(env_override).expanduser())
38
+
39
+ if is_root:
40
+ if not sudo_user or sudo_user == "root":
41
+ candidates.append(default)
42
+ candidates.append(Path("/var/log/arthexis"))
43
+ candidates.append(Path("/tmp/arthexis/logs"))
44
+ else:
45
+ home: Path | None
46
+ try:
47
+ home = Path.home()
48
+ except (RuntimeError, OSError, KeyError):
49
+ home = None
50
+
51
+ candidates.append(default)
52
+
53
+ tmp_logs = Path(tempfile.gettempdir()) / "arthexis" / "logs"
54
+
55
+ if home is not None:
56
+ state_home = _state_home(home)
57
+ candidates.extend(
58
+ [
59
+ state_home / "arthexis" / "logs",
60
+ home / ".arthexis" / "logs",
61
+ ]
62
+ )
63
+ else:
64
+ candidates.append(tmp_logs)
65
+
66
+ candidates.append(Path("/tmp/arthexis/logs"))
67
+ candidates.append(tmp_logs)
68
+
69
+ seen: set[Path] = set()
70
+ ordered_candidates: list[Path] = []
71
+ for candidate in candidates:
72
+ candidate = candidate.expanduser()
73
+ if candidate not in seen:
74
+ seen.add(candidate)
75
+ ordered_candidates.append(candidate)
76
+
77
+ attempted: list[Path] = []
78
+ chosen: Path | None = None
79
+ for candidate in ordered_candidates:
80
+ attempted.append(candidate)
81
+ try:
82
+ candidate.mkdir(parents=True, exist_ok=True)
83
+ except OSError:
84
+ continue
85
+ if os.access(candidate, os.W_OK | os.X_OK):
86
+ chosen = candidate
87
+ break
88
+
89
+ if chosen is None:
90
+ attempted_str = (
91
+ ", ".join(str(path) for path in attempted) if attempted else "none"
92
+ )
93
+ raise RuntimeError(
94
+ f"Unable to create a writable log directory. Tried: {attempted_str}"
95
+ )
96
+
97
+ if chosen != default:
98
+ if (
99
+ attempted
100
+ and attempted[0] == default
101
+ and not os.access(default, os.W_OK | os.X_OK)
102
+ ):
103
+ print(
104
+ f"Log directory {default} is not writable; using {chosen}",
105
+ file=sys.stderr,
106
+ )
107
+ elif is_root and sudo_user and sudo_user != "root" and not env_override:
108
+ print(
109
+ f"Running with elevated privileges; writing logs to {chosen}",
110
+ file=sys.stderr,
111
+ )
112
+
113
+ os.environ["ARTHEXIS_LOG_DIR"] = str(chosen)
114
+ return chosen
core/mailer.py CHANGED
@@ -1,83 +1,89 @@
1
- import logging
2
- from typing import Sequence
3
-
4
- from django.conf import settings
5
- from django.core.mail import EmailMessage
6
- from django.utils.module_loading import import_string
7
-
8
- try: # pragma: no cover - import should always succeed but guard defensively
9
- from django.core.mail.backends.dummy import (
10
- EmailBackend as DummyEmailBackend,
11
- )
12
- except Exception: # pragma: no cover - fallback when dummy backend unavailable
13
- DummyEmailBackend = None # type: ignore[assignment]
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- def send(
19
- subject: str,
20
- message: str,
21
- recipient_list: Sequence[str],
22
- from_email: str | None = None,
23
- *,
24
- outbox=None,
25
- attachments: Sequence[tuple[str, str, str]] | None = None,
26
- content_subtype: str | None = None,
27
- **kwargs,
28
- ):
29
- """Send an email using Django's email utilities.
30
-
31
- If ``outbox`` is provided, its connection will be used when sending.
32
- """
33
- sender = (
34
- from_email or getattr(outbox, "from_email", None) or settings.DEFAULT_FROM_EMAIL
35
- )
36
- connection = outbox.get_connection() if outbox is not None else None
37
- fail_silently = kwargs.pop("fail_silently", False)
38
- email = EmailMessage(
39
- subject=subject,
40
- body=message,
41
- from_email=sender,
42
- to=list(recipient_list),
43
- connection=connection,
44
- **kwargs,
45
- )
46
- if attachments:
47
- for attachment in attachments:
48
- if not isinstance(attachment, (list, tuple)) or len(attachment) != 3:
49
- raise ValueError(
50
- "attachments must contain (name, content, mimetype) tuples"
51
- )
52
- email.attach(*attachment)
53
- if content_subtype:
54
- email.content_subtype = content_subtype
55
- email.send(fail_silently=fail_silently)
56
- return email
57
-
58
-
59
- def can_send_email() -> bool:
60
- """Return ``True`` when at least one outbound email path is configured."""
61
-
62
- from nodes.models import EmailOutbox # imported lazily to avoid circular deps
63
-
64
- has_outbox = EmailOutbox.objects.exclude(host="").exists()
65
- if has_outbox:
66
- return True
67
-
68
- backend_path = getattr(settings, "EMAIL_BACKEND", "")
69
- if not backend_path:
70
- return False
71
- try:
72
- backend_cls = import_string(backend_path)
73
- except Exception: # pragma: no cover - misconfigured backend
74
- logger.warning("Email backend %s could not be imported", backend_path)
75
- return False
76
-
77
- if DummyEmailBackend is None:
78
- return True
79
- try:
80
- return not issubclass(backend_cls, DummyEmailBackend)
81
- except TypeError: # pragma: no cover - backend not a class
82
- logger.warning("Email backend %s is not a class", backend_path)
83
- return False
1
+ import logging
2
+ from typing import Sequence
3
+
4
+ from django.conf import settings
5
+ from django.core.mail import EmailMessage
6
+ from django.utils.module_loading import import_string
7
+
8
+ try: # pragma: no cover - import should always succeed but guard defensively
9
+ from django.core.mail.backends.dummy import (
10
+ EmailBackend as DummyEmailBackend,
11
+ )
12
+ except Exception: # pragma: no cover - fallback when dummy backend unavailable
13
+ DummyEmailBackend = None # type: ignore[assignment]
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def send(
19
+ subject: str,
20
+ message: str,
21
+ recipient_list: Sequence[str],
22
+ from_email: str | None = None,
23
+ *,
24
+ outbox=None,
25
+ attachments: Sequence[tuple[str, str, str]] | None = None,
26
+ content_subtype: str | None = None,
27
+ **kwargs,
28
+ ):
29
+ """Send an email using Django's email utilities.
30
+
31
+ If ``outbox`` is provided, its connection will be used when sending.
32
+ """
33
+ sender = (
34
+ from_email or getattr(outbox, "from_email", None) or settings.DEFAULT_FROM_EMAIL
35
+ )
36
+ connection = outbox.get_connection() if outbox is not None else None
37
+ fail_silently = kwargs.pop("fail_silently", False)
38
+ email = EmailMessage(
39
+ subject=subject,
40
+ body=message,
41
+ from_email=sender,
42
+ to=list(recipient_list),
43
+ connection=connection,
44
+ **kwargs,
45
+ )
46
+ if attachments:
47
+ for attachment in attachments:
48
+ if isinstance(attachment, (list, tuple)):
49
+ length = len(attachment)
50
+ if length not in {2, 3}:
51
+ raise ValueError(
52
+ "attachments must contain 2- or 3-item (name, content, mimetype) tuples"
53
+ )
54
+ email.attach(*attachment)
55
+ else:
56
+ email.attach(attachment)
57
+ if content_subtype:
58
+ email.content_subtype = content_subtype
59
+ email.send(fail_silently=fail_silently)
60
+ return email
61
+
62
+
63
+ def can_send_email() -> bool:
64
+ """Return ``True`` when at least one outbound email path is configured."""
65
+
66
+ from nodes.models import EmailOutbox # imported lazily to avoid circular deps
67
+
68
+ has_outbox = (
69
+ EmailOutbox.objects.filter(is_enabled=True).exclude(host="").exists()
70
+ )
71
+ if has_outbox:
72
+ return True
73
+
74
+ backend_path = getattr(settings, "EMAIL_BACKEND", "")
75
+ if not backend_path:
76
+ return False
77
+ try:
78
+ backend_cls = import_string(backend_path)
79
+ except Exception: # pragma: no cover - misconfigured backend
80
+ logger.warning("Email backend %s could not be imported", backend_path)
81
+ return False
82
+
83
+ if DummyEmailBackend is None:
84
+ return True
85
+ try:
86
+ return not issubclass(backend_cls, DummyEmailBackend)
87
+ except TypeError: # pragma: no cover - backend not a class
88
+ logger.warning("Email backend %s is not a class", backend_path)
89
+ return False
core/middleware.py CHANGED
@@ -1,91 +1,91 @@
1
- from django.contrib.auth import get_user_model
2
- from django.contrib.contenttypes.models import ContentType
3
- from django.contrib.sites.models import Site
4
- from django.urls import resolve
5
-
6
- from nodes.models import Node, NodeRole
7
-
8
- from .models import AdminHistory
9
- from .sigil_context import set_context, clear_context
10
-
11
-
12
- class AdminHistoryMiddleware:
13
- """Log recently visited admin changelists for each user."""
14
-
15
- def __init__(self, get_response):
16
- self.get_response = get_response
17
-
18
- def __call__(self, request):
19
- response = self.get_response(request)
20
- match = getattr(request, "resolver_match", None)
21
- if (
22
- request.user.is_authenticated
23
- and request.user.is_staff
24
- and request.method == "GET"
25
- and match
26
- and match.url_name
27
- and match.url_name.endswith("_changelist")
28
- and response.status_code == 200
29
- ):
30
- parts = request.path.strip("/").split("/")
31
- if len(parts) >= 3:
32
- app_label, model_name = parts[1], parts[2]
33
- content_type = ContentType.objects.get_by_natural_key(
34
- app_label, model_name
35
- )
36
- AdminHistory.objects.update_or_create(
37
- user=request.user,
38
- url=request.get_full_path(),
39
- defaults={"content_type": content_type},
40
- )
41
- return response
42
-
43
-
44
- class SigilContextMiddleware:
45
- """Capture model instance identifiers from resolved views."""
46
-
47
- def __init__(self, get_response):
48
- self.get_response = get_response
49
-
50
- def __call__(self, request):
51
- context = {}
52
- if request.user.is_authenticated:
53
- context[get_user_model()] = request.user.pk
54
- try:
55
- site = Site.objects.get_current(request)
56
- context[Site] = site.pk
57
- except Exception:
58
- pass
59
- if hasattr(request, "node") and getattr(request, "node", None):
60
- context[Node] = request.node.pk
61
- if hasattr(request, "role") and getattr(request, "role", None):
62
- context[NodeRole] = request.role.pk
63
- try:
64
- match = resolve(request.path_info)
65
- except Exception: # pragma: no cover - resolution errors
66
- match = None
67
- if match and hasattr(match, "func"):
68
- view = match.func
69
- model = None
70
- if hasattr(view, "view_class"):
71
- view_class = view.view_class
72
- model = getattr(view_class, "model", None)
73
- if model is None:
74
- queryset = getattr(view_class, "queryset", None)
75
- if queryset is not None:
76
- model = queryset.model
77
- if model is not None:
78
- pk = match.kwargs.get("pk") or match.kwargs.get("id")
79
- if pk is not None:
80
- context[model] = pk
81
- for field in model._meta.fields:
82
- if field.is_relation:
83
- for key in (field.name, field.attname):
84
- if key in match.kwargs:
85
- context[field.related_model] = match.kwargs[key]
86
- break
87
- set_context(context)
88
- try:
89
- return self.get_response(request)
90
- finally:
91
- clear_context()
1
+ from django.contrib.auth import get_user_model
2
+ from django.contrib.contenttypes.models import ContentType
3
+ from django.contrib.sites.models import Site
4
+ from django.urls import resolve
5
+
6
+ from nodes.models import Node, NodeRole
7
+
8
+ from .models import AdminHistory
9
+ from .sigil_context import set_context, clear_context
10
+
11
+
12
+ class AdminHistoryMiddleware:
13
+ """Log recently visited admin changelists for each user."""
14
+
15
+ def __init__(self, get_response):
16
+ self.get_response = get_response
17
+
18
+ def __call__(self, request):
19
+ response = self.get_response(request)
20
+ match = getattr(request, "resolver_match", None)
21
+ if (
22
+ request.user.is_authenticated
23
+ and request.user.is_staff
24
+ and request.method == "GET"
25
+ and match
26
+ and match.url_name
27
+ and match.url_name.endswith("_changelist")
28
+ and response.status_code == 200
29
+ ):
30
+ parts = request.path.strip("/").split("/")
31
+ if len(parts) >= 3:
32
+ app_label, model_name = parts[1], parts[2]
33
+ content_type = ContentType.objects.get_by_natural_key(
34
+ app_label, model_name
35
+ )
36
+ AdminHistory.objects.update_or_create(
37
+ user=request.user,
38
+ url=request.get_full_path(),
39
+ defaults={"content_type": content_type},
40
+ )
41
+ return response
42
+
43
+
44
+ class SigilContextMiddleware:
45
+ """Capture model instance identifiers from resolved views."""
46
+
47
+ def __init__(self, get_response):
48
+ self.get_response = get_response
49
+
50
+ def __call__(self, request):
51
+ context = {}
52
+ if request.user.is_authenticated:
53
+ context[get_user_model()] = request.user.pk
54
+ try:
55
+ site = Site.objects.get_current(request)
56
+ context[Site] = site.pk
57
+ except Exception:
58
+ pass
59
+ if hasattr(request, "node") and getattr(request, "node", None):
60
+ context[Node] = request.node.pk
61
+ if hasattr(request, "role") and getattr(request, "role", None):
62
+ context[NodeRole] = request.role.pk
63
+ try:
64
+ match = resolve(request.path_info)
65
+ except Exception: # pragma: no cover - resolution errors
66
+ match = None
67
+ if match and hasattr(match, "func"):
68
+ view = match.func
69
+ model = None
70
+ if hasattr(view, "view_class"):
71
+ view_class = view.view_class
72
+ model = getattr(view_class, "model", None)
73
+ if model is None:
74
+ queryset = getattr(view_class, "queryset", None)
75
+ if queryset is not None:
76
+ model = queryset.model
77
+ if model is not None:
78
+ pk = match.kwargs.get("pk") or match.kwargs.get("id")
79
+ if pk is not None:
80
+ context[model] = pk
81
+ for field in model._meta.fields:
82
+ if field.is_relation:
83
+ for key in (field.name, field.attname):
84
+ if key in match.kwargs:
85
+ context[field.related_model] = match.kwargs[key]
86
+ break
87
+ set_context(context)
88
+ try:
89
+ return self.get_response(request)
90
+ finally:
91
+ clear_context()