arthexis 0.1.13__py3-none-any.whl → 0.1.14__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.13.dist-info → arthexis-0.1.14.dist-info}/METADATA +222 -221
- arthexis-0.1.14.dist-info/RECORD +109 -0
- {arthexis-0.1.13.dist-info → arthexis-0.1.14.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +43 -43
- config/auth_app.py +7 -7
- config/celery.py +32 -32
- config/context_processors.py +67 -69
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +25 -25
- config/offline.py +49 -49
- config/settings.py +691 -682
- config/settings_helpers.py +109 -109
- config/urls.py +171 -166
- config/wsgi.py +17 -17
- core/admin.py +3771 -2809
- core/admin_history.py +50 -50
- core/admindocs.py +151 -151
- core/apps.py +356 -272
- core/auto_upgrade.py +57 -57
- core/backends.py +265 -236
- core/changelog.py +342 -0
- core/entity.py +133 -133
- core/environment.py +61 -61
- core/fields.py +168 -168
- core/form_fields.py +75 -75
- core/github_helper.py +188 -25
- core/github_issues.py +178 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +100 -100
- core/mailer.py +85 -85
- core/middleware.py +91 -91
- core/models.py +3609 -2795
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +108 -108
- core/release.py +721 -368
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -149
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +315 -315
- core/system.py +752 -493
- core/tasks.py +408 -394
- core/temp_passwords.py +181 -181
- core/test_system_info.py +186 -139
- core/tests.py +2095 -1521
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +641 -633
- core/views.py +2175 -1417
- core/widgets.py +213 -94
- core/workgroup_urls.py +17 -17
- core/workgroup_views.py +94 -94
- nodes/admin.py +1720 -1161
- nodes/apps.py +87 -85
- nodes/backends.py +160 -160
- nodes/dns.py +203 -203
- nodes/feature_checks.py +133 -133
- nodes/lcd.py +165 -165
- nodes/models.py +1737 -1597
- nodes/reports.py +411 -411
- nodes/rfid_sync.py +195 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +46 -46
- nodes/tests.py +3810 -3116
- nodes/urls.py +15 -14
- nodes/utils.py +121 -105
- nodes/views.py +683 -619
- ocpp/admin.py +948 -948
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1565 -1459
- ocpp/evcs.py +844 -844
- ocpp/evcs_discovery.py +158 -158
- ocpp/models.py +917 -917
- ocpp/reference_utils.py +42 -42
- ocpp/routing.py +11 -11
- ocpp/simulator.py +745 -745
- ocpp/status_display.py +26 -26
- ocpp/store.py +601 -541
- ocpp/tasks.py +31 -31
- ocpp/test_export_import.py +130 -130
- ocpp/test_rfid.py +913 -702
- ocpp/tests.py +4445 -4094
- ocpp/transactions_io.py +189 -189
- ocpp/urls.py +50 -50
- ocpp/views.py +1479 -1251
- pages/admin.py +708 -539
- pages/apps.py +10 -10
- pages/checks.py +40 -40
- pages/context_processors.py +127 -119
- pages/defaults.py +13 -13
- pages/forms.py +198 -198
- pages/middleware.py +205 -153
- pages/models.py +607 -426
- pages/tests.py +2612 -2200
- pages/urls.py +25 -25
- pages/utils.py +12 -12
- pages/views.py +1165 -1128
- arthexis-0.1.13.dist-info/RECORD +0 -105
- nodes/actions.py +0 -70
- {arthexis-0.1.13.dist-info → arthexis-0.1.14.dist-info}/WHEEL +0 -0
- {arthexis-0.1.13.dist-info → arthexis-0.1.14.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
|
+
|