arthexis 0.1.13__py3-none-any.whl → 0.1.15__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.
Files changed (108) hide show
  1. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/METADATA +224 -221
  2. arthexis-0.1.15.dist-info/RECORD +110 -0
  3. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +43 -43
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -32
  9. config/context_processors.py +67 -69
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +25 -25
  14. config/offline.py +49 -49
  15. config/settings.py +691 -682
  16. config/settings_helpers.py +109 -109
  17. config/urls.py +171 -166
  18. config/wsgi.py +17 -17
  19. core/admin.py +3795 -2809
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +151 -151
  22. core/apps.py +356 -272
  23. core/auto_upgrade.py +57 -57
  24. core/backends.py +265 -236
  25. core/changelog.py +342 -0
  26. core/entity.py +149 -133
  27. core/environment.py +61 -61
  28. core/fields.py +168 -168
  29. core/form_fields.py +75 -75
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +178 -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 +85 -85
  37. core/middleware.py +91 -91
  38. core/models.py +3637 -2795
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +108 -108
  42. core/release.py +840 -368
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -149
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +315 -315
  47. core/system.py +952 -493
  48. core/tasks.py +408 -394
  49. core/temp_passwords.py +181 -181
  50. core/test_system_info.py +186 -139
  51. core/tests.py +2168 -1521
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +641 -633
  55. core/views.py +2201 -1417
  56. core/widgets.py +213 -94
  57. core/workgroup_urls.py +17 -17
  58. core/workgroup_views.py +94 -94
  59. nodes/admin.py +1720 -1161
  60. nodes/apps.py +87 -85
  61. nodes/backends.py +160 -160
  62. nodes/dns.py +203 -203
  63. nodes/feature_checks.py +133 -133
  64. nodes/lcd.py +165 -165
  65. nodes/models.py +1764 -1597
  66. nodes/reports.py +411 -411
  67. nodes/rfid_sync.py +195 -0
  68. nodes/signals.py +18 -0
  69. nodes/tasks.py +46 -46
  70. nodes/tests.py +3830 -3116
  71. nodes/urls.py +15 -14
  72. nodes/utils.py +121 -105
  73. nodes/views.py +683 -619
  74. ocpp/admin.py +948 -948
  75. ocpp/apps.py +25 -25
  76. ocpp/consumers.py +1565 -1459
  77. ocpp/evcs.py +844 -844
  78. ocpp/evcs_discovery.py +158 -158
  79. ocpp/models.py +917 -917
  80. ocpp/reference_utils.py +42 -42
  81. ocpp/routing.py +11 -11
  82. ocpp/simulator.py +745 -745
  83. ocpp/status_display.py +26 -26
  84. ocpp/store.py +601 -541
  85. ocpp/tasks.py +31 -31
  86. ocpp/test_export_import.py +130 -130
  87. ocpp/test_rfid.py +913 -702
  88. ocpp/tests.py +4445 -4094
  89. ocpp/transactions_io.py +189 -189
  90. ocpp/urls.py +50 -50
  91. ocpp/views.py +1479 -1251
  92. pages/admin.py +769 -539
  93. pages/apps.py +10 -10
  94. pages/checks.py +40 -40
  95. pages/context_processors.py +127 -119
  96. pages/defaults.py +13 -13
  97. pages/forms.py +198 -198
  98. pages/middleware.py +209 -153
  99. pages/models.py +643 -426
  100. pages/tasks.py +74 -0
  101. pages/tests.py +3025 -2200
  102. pages/urls.py +26 -25
  103. pages/utils.py +23 -12
  104. pages/views.py +1176 -1128
  105. arthexis-0.1.13.dist-info/RECORD +0 -105
  106. nodes/actions.py +0 -70
  107. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/WHEEL +0 -0
  108. {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/top_level.txt +0 -0
core/temp_passwords.py CHANGED
@@ -1,181 +1,181 @@
1
- """Utilities for temporary password lock files."""
2
-
3
- from __future__ import annotations
4
-
5
- import hashlib
6
- import json
7
- import re
8
- import secrets
9
- import string
10
- from dataclasses import dataclass
11
- from datetime import datetime, timedelta
12
- from pathlib import Path
13
- from typing import Optional
14
-
15
- from django.conf import settings
16
- from django.contrib.auth.hashers import check_password, make_password
17
- from django.utils import timezone
18
- from django.utils.dateparse import parse_datetime
19
-
20
-
21
- DEFAULT_PASSWORD_LENGTH = 16
22
- DEFAULT_EXPIRATION = timedelta(hours=1)
23
- _SAFE_COMPONENT_RE = re.compile(r"[^A-Za-z0-9_.-]+")
24
-
25
-
26
- def _base_lock_dir() -> Path:
27
- """Return the root directory used for temporary password lock files."""
28
-
29
- configured = getattr(settings, "TEMP_PASSWORD_LOCK_DIR", None)
30
- if configured:
31
- path = Path(configured)
32
- else:
33
- path = Path(settings.BASE_DIR) / "locks" / "temp-passwords"
34
- path.mkdir(parents=True, exist_ok=True)
35
- return path
36
-
37
-
38
- def _safe_component(value: str) -> str:
39
- """Return a filesystem safe component derived from ``value``."""
40
-
41
- if not value:
42
- return ""
43
- safe = _SAFE_COMPONENT_RE.sub("_", value)
44
- safe = safe.strip("._")
45
- return safe[:64]
46
-
47
-
48
- def _lockfile_name(username: str) -> str:
49
- """Return the filename used for the provided ``username``."""
50
-
51
- digest = hashlib.sha256(username.encode("utf-8")).hexdigest()[:12]
52
- safe = _safe_component(username)
53
- if safe:
54
- return f"{safe}-{digest}.json"
55
- return f"user-{digest}.json"
56
-
57
-
58
- def _lockfile_path(username: str) -> Path:
59
- """Return the lockfile path for ``username``."""
60
-
61
- return _base_lock_dir() / _lockfile_name(username)
62
-
63
-
64
- def _parse_timestamp(value: str | None) -> Optional[datetime]:
65
- """Return a timezone aware datetime parsed from ``value``."""
66
-
67
- if not value:
68
- return None
69
- parsed = parse_datetime(value)
70
- if parsed is None:
71
- return None
72
- if timezone.is_naive(parsed):
73
- parsed = timezone.make_aware(parsed)
74
- return parsed
75
-
76
-
77
- @dataclass(frozen=True)
78
- class TempPasswordEntry:
79
- """Details for a temporary password stored on disk."""
80
-
81
- username: str
82
- password_hash: str
83
- expires_at: datetime
84
- created_at: datetime
85
- path: Path
86
- allow_change: bool = False
87
-
88
- @property
89
- def is_expired(self) -> bool:
90
- return timezone.now() >= self.expires_at
91
-
92
- def check_password(self, raw_password: str) -> bool:
93
- """Return ``True`` if ``raw_password`` matches this entry."""
94
-
95
- return check_password(raw_password, self.password_hash)
96
-
97
-
98
- def generate_password(length: int = DEFAULT_PASSWORD_LENGTH) -> str:
99
- """Return a random password composed of letters and digits."""
100
-
101
- if length <= 0:
102
- raise ValueError("length must be a positive integer")
103
- alphabet = string.ascii_letters + string.digits
104
- return "".join(secrets.choice(alphabet) for _ in range(length))
105
-
106
-
107
- def store_temp_password(
108
- username: str,
109
- raw_password: str,
110
- expires_at: Optional[datetime] = None,
111
- *,
112
- allow_change: bool = False,
113
- ) -> TempPasswordEntry:
114
- """Persist a temporary password for ``username`` and return the entry."""
115
-
116
- if expires_at is None:
117
- expires_at = timezone.now() + DEFAULT_EXPIRATION
118
- if timezone.is_naive(expires_at):
119
- expires_at = timezone.make_aware(expires_at)
120
- created_at = timezone.now()
121
- path = _lockfile_path(username)
122
- data = {
123
- "username": username,
124
- "password_hash": make_password(raw_password),
125
- "expires_at": expires_at.isoformat(),
126
- "created_at": created_at.isoformat(),
127
- "allow_change": allow_change,
128
- }
129
- path.write_text(json.dumps(data, indent=2, sort_keys=True))
130
- return TempPasswordEntry(
131
- username=username,
132
- password_hash=data["password_hash"],
133
- expires_at=expires_at,
134
- created_at=created_at,
135
- path=path,
136
- allow_change=allow_change,
137
- )
138
-
139
-
140
- def load_temp_password(username: str) -> Optional[TempPasswordEntry]:
141
- """Return the stored temporary password for ``username``, if any."""
142
-
143
- path = _lockfile_path(username)
144
- if not path.exists():
145
- return None
146
- try:
147
- data = json.loads(path.read_text())
148
- except (json.JSONDecodeError, UnicodeDecodeError):
149
- path.unlink(missing_ok=True)
150
- return None
151
-
152
- expires_at = _parse_timestamp(data.get("expires_at"))
153
- created_at = _parse_timestamp(data.get("created_at")) or timezone.now()
154
- password_hash = data.get("password_hash")
155
- if not expires_at or not password_hash:
156
- path.unlink(missing_ok=True)
157
- return None
158
-
159
- username = data.get("username") or username
160
- allow_change_value = data.get("allow_change", False)
161
- if isinstance(allow_change_value, str):
162
- allow_change = allow_change_value.lower() in {"1", "true", "yes", "on"}
163
- else:
164
- allow_change = bool(allow_change_value)
165
-
166
- return TempPasswordEntry(
167
- username=username,
168
- password_hash=password_hash,
169
- expires_at=expires_at,
170
- created_at=created_at,
171
- path=path,
172
- allow_change=allow_change,
173
- )
174
-
175
-
176
- def discard_temp_password(username: str) -> None:
177
- """Remove any stored temporary password for ``username``."""
178
-
179
- path = _lockfile_path(username)
180
- path.unlink(missing_ok=True)
181
-
1
+ """Utilities for temporary password lock files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ import secrets
9
+ import string
10
+ from dataclasses import dataclass
11
+ from datetime import datetime, timedelta
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ from django.conf import settings
16
+ from django.contrib.auth.hashers import check_password, make_password
17
+ from django.utils import timezone
18
+ from django.utils.dateparse import parse_datetime
19
+
20
+
21
+ DEFAULT_PASSWORD_LENGTH = 16
22
+ DEFAULT_EXPIRATION = timedelta(hours=1)
23
+ _SAFE_COMPONENT_RE = re.compile(r"[^A-Za-z0-9_.-]+")
24
+
25
+
26
+ def _base_lock_dir() -> Path:
27
+ """Return the root directory used for temporary password lock files."""
28
+
29
+ configured = getattr(settings, "TEMP_PASSWORD_LOCK_DIR", None)
30
+ if configured:
31
+ path = Path(configured)
32
+ else:
33
+ path = Path(settings.BASE_DIR) / "locks" / "temp-passwords"
34
+ path.mkdir(parents=True, exist_ok=True)
35
+ return path
36
+
37
+
38
+ def _safe_component(value: str) -> str:
39
+ """Return a filesystem safe component derived from ``value``."""
40
+
41
+ if not value:
42
+ return ""
43
+ safe = _SAFE_COMPONENT_RE.sub("_", value)
44
+ safe = safe.strip("._")
45
+ return safe[:64]
46
+
47
+
48
+ def _lockfile_name(username: str) -> str:
49
+ """Return the filename used for the provided ``username``."""
50
+
51
+ digest = hashlib.sha256(username.encode("utf-8")).hexdigest()[:12]
52
+ safe = _safe_component(username)
53
+ if safe:
54
+ return f"{safe}-{digest}.json"
55
+ return f"user-{digest}.json"
56
+
57
+
58
+ def _lockfile_path(username: str) -> Path:
59
+ """Return the lockfile path for ``username``."""
60
+
61
+ return _base_lock_dir() / _lockfile_name(username)
62
+
63
+
64
+ def _parse_timestamp(value: str | None) -> Optional[datetime]:
65
+ """Return a timezone aware datetime parsed from ``value``."""
66
+
67
+ if not value:
68
+ return None
69
+ parsed = parse_datetime(value)
70
+ if parsed is None:
71
+ return None
72
+ if timezone.is_naive(parsed):
73
+ parsed = timezone.make_aware(parsed)
74
+ return parsed
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class TempPasswordEntry:
79
+ """Details for a temporary password stored on disk."""
80
+
81
+ username: str
82
+ password_hash: str
83
+ expires_at: datetime
84
+ created_at: datetime
85
+ path: Path
86
+ allow_change: bool = False
87
+
88
+ @property
89
+ def is_expired(self) -> bool:
90
+ return timezone.now() >= self.expires_at
91
+
92
+ def check_password(self, raw_password: str) -> bool:
93
+ """Return ``True`` if ``raw_password`` matches this entry."""
94
+
95
+ return check_password(raw_password, self.password_hash)
96
+
97
+
98
+ def generate_password(length: int = DEFAULT_PASSWORD_LENGTH) -> str:
99
+ """Return a random password composed of letters and digits."""
100
+
101
+ if length <= 0:
102
+ raise ValueError("length must be a positive integer")
103
+ alphabet = string.ascii_letters + string.digits
104
+ return "".join(secrets.choice(alphabet) for _ in range(length))
105
+
106
+
107
+ def store_temp_password(
108
+ username: str,
109
+ raw_password: str,
110
+ expires_at: Optional[datetime] = None,
111
+ *,
112
+ allow_change: bool = False,
113
+ ) -> TempPasswordEntry:
114
+ """Persist a temporary password for ``username`` and return the entry."""
115
+
116
+ if expires_at is None:
117
+ expires_at = timezone.now() + DEFAULT_EXPIRATION
118
+ if timezone.is_naive(expires_at):
119
+ expires_at = timezone.make_aware(expires_at)
120
+ created_at = timezone.now()
121
+ path = _lockfile_path(username)
122
+ data = {
123
+ "username": username,
124
+ "password_hash": make_password(raw_password),
125
+ "expires_at": expires_at.isoformat(),
126
+ "created_at": created_at.isoformat(),
127
+ "allow_change": allow_change,
128
+ }
129
+ path.write_text(json.dumps(data, indent=2, sort_keys=True))
130
+ return TempPasswordEntry(
131
+ username=username,
132
+ password_hash=data["password_hash"],
133
+ expires_at=expires_at,
134
+ created_at=created_at,
135
+ path=path,
136
+ allow_change=allow_change,
137
+ )
138
+
139
+
140
+ def load_temp_password(username: str) -> Optional[TempPasswordEntry]:
141
+ """Return the stored temporary password for ``username``, if any."""
142
+
143
+ path = _lockfile_path(username)
144
+ if not path.exists():
145
+ return None
146
+ try:
147
+ data = json.loads(path.read_text())
148
+ except (json.JSONDecodeError, UnicodeDecodeError):
149
+ path.unlink(missing_ok=True)
150
+ return None
151
+
152
+ expires_at = _parse_timestamp(data.get("expires_at"))
153
+ created_at = _parse_timestamp(data.get("created_at")) or timezone.now()
154
+ password_hash = data.get("password_hash")
155
+ if not expires_at or not password_hash:
156
+ path.unlink(missing_ok=True)
157
+ return None
158
+
159
+ username = data.get("username") or username
160
+ allow_change_value = data.get("allow_change", False)
161
+ if isinstance(allow_change_value, str):
162
+ allow_change = allow_change_value.lower() in {"1", "true", "yes", "on"}
163
+ else:
164
+ allow_change = bool(allow_change_value)
165
+
166
+ return TempPasswordEntry(
167
+ username=username,
168
+ password_hash=password_hash,
169
+ expires_at=expires_at,
170
+ created_at=created_at,
171
+ path=path,
172
+ allow_change=allow_change,
173
+ )
174
+
175
+
176
+ def discard_temp_password(username: str) -> None:
177
+ """Remove any stored temporary password for ``username``."""
178
+
179
+ path = _lockfile_path(username)
180
+ path.unlink(missing_ok=True)
181
+