arthexis 0.1.6__py3-none-any.whl → 0.1.8__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.
- {arthexis-0.1.6.dist-info → arthexis-0.1.8.dist-info}/METADATA +12 -8
- {arthexis-0.1.6.dist-info → arthexis-0.1.8.dist-info}/RECORD +36 -29
- config/celery.py +7 -0
- config/horologia_app.py +7 -0
- config/logging.py +8 -3
- config/settings.py +3 -2
- config/urls.py +9 -0
- config/workgroup_app.py +7 -0
- core/admin.py +192 -17
- core/admindocs.py +44 -0
- core/apps.py +2 -1
- core/checks.py +29 -0
- core/entity.py +29 -7
- core/models.py +124 -14
- core/release.py +29 -141
- core/system.py +2 -2
- core/test_system_info.py +21 -0
- core/tests.py +292 -1
- core/views.py +153 -134
- core/workgroup_urls.py +13 -0
- core/workgroup_views.py +57 -0
- nodes/admin.py +211 -0
- nodes/apps.py +1 -1
- nodes/models.py +103 -7
- nodes/tests.py +27 -0
- ocpp/apps.py +4 -3
- ocpp/models.py +1 -1
- ocpp/simulator.py +4 -0
- ocpp/tests.py +5 -1
- pages/admin.py +8 -3
- pages/apps.py +1 -1
- pages/tests.py +23 -4
- pages/views.py +22 -3
- {arthexis-0.1.6.dist-info → arthexis-0.1.8.dist-info}/WHEEL +0 -0
- {arthexis-0.1.6.dist-info → arthexis-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.6.dist-info → arthexis-0.1.8.dist-info}/top_level.txt +0 -0
core/checks.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.core import checks
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _fixture_hash() -> str:
|
|
8
|
+
base_dir = Path(settings.BASE_DIR)
|
|
9
|
+
md5 = hashlib.md5()
|
|
10
|
+
for path in sorted(base_dir.glob("**/fixtures/*.json")):
|
|
11
|
+
md5.update(path.read_bytes())
|
|
12
|
+
return md5.hexdigest()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@checks.register(checks.Tags.database)
|
|
16
|
+
def check_unapplied_fixtures(app_configs=None, **kwargs):
|
|
17
|
+
"""Warn if fixture files have changed since last refresh."""
|
|
18
|
+
hash_file = Path(settings.BASE_DIR) / "fixtures.md5"
|
|
19
|
+
stored = hash_file.read_text().strip() if hash_file.exists() else ""
|
|
20
|
+
current = _fixture_hash()
|
|
21
|
+
if stored != current:
|
|
22
|
+
return [
|
|
23
|
+
checks.Warning(
|
|
24
|
+
"Unapplied fixture changes detected.",
|
|
25
|
+
hint="Run env-refresh to apply fixtures.",
|
|
26
|
+
id="core.W001",
|
|
27
|
+
)
|
|
28
|
+
]
|
|
29
|
+
return []
|
core/entity.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
import logging
|
|
2
3
|
import os
|
|
3
4
|
import re
|
|
4
5
|
|
|
@@ -7,6 +8,8 @@ from django.conf import settings
|
|
|
7
8
|
from django.db import models
|
|
8
9
|
from django.contrib.auth.models import UserManager as DjangoUserManager
|
|
9
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
class EntityQuerySet(models.QuerySet):
|
|
12
15
|
def delete(self): # pragma: no cover - delegates to instance delete
|
|
@@ -78,14 +81,33 @@ class Entity(models.Model):
|
|
|
78
81
|
root_name, key = match.group(1), match.group(2)
|
|
79
82
|
try:
|
|
80
83
|
root = SigilRoot.objects.get(prefix__iexact=root_name)
|
|
84
|
+
if root.context_type == SigilRoot.Context.CONFIG:
|
|
85
|
+
if root.prefix.upper() == "ENV":
|
|
86
|
+
if key in os.environ:
|
|
87
|
+
return os.environ[key]
|
|
88
|
+
logger.warning(
|
|
89
|
+
"Missing environment variable for sigil [%s.%s]",
|
|
90
|
+
root_name,
|
|
91
|
+
key,
|
|
92
|
+
)
|
|
93
|
+
return match.group(0)
|
|
94
|
+
if root.prefix.upper() == "SYS":
|
|
95
|
+
if hasattr(settings, key):
|
|
96
|
+
return str(getattr(settings, key))
|
|
97
|
+
logger.warning(
|
|
98
|
+
"Missing settings attribute for sigil [%s.%s]",
|
|
99
|
+
root_name,
|
|
100
|
+
key,
|
|
101
|
+
)
|
|
102
|
+
return match.group(0)
|
|
103
|
+
logger.warning(
|
|
104
|
+
"Unresolvable sigil [%s.%s]: unsupported context", root_name, key
|
|
105
|
+
)
|
|
81
106
|
except SigilRoot.DoesNotExist:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if root.prefix.upper() == "SYS":
|
|
87
|
-
return str(getattr(settings, key, ""))
|
|
88
|
-
return ""
|
|
107
|
+
logger.warning("Unknown sigil root [%s]", root_name)
|
|
108
|
+
except Exception:
|
|
109
|
+
logger.exception("Error resolving sigil [%s.%s]", root_name, key)
|
|
110
|
+
return match.group(0)
|
|
89
111
|
|
|
90
112
|
return pattern.sub(repl, text)
|
|
91
113
|
|
core/models.py
CHANGED
|
@@ -10,12 +10,14 @@ from django.utils.translation import gettext_lazy as _
|
|
|
10
10
|
from django.core.validators import RegexValidator
|
|
11
11
|
from django.core.exceptions import ValidationError
|
|
12
12
|
from django.apps import apps
|
|
13
|
-
from django.db.models.signals import m2m_changed
|
|
13
|
+
from django.db.models.signals import m2m_changed
|
|
14
14
|
from django.dispatch import receiver
|
|
15
15
|
from datetime import timedelta
|
|
16
16
|
from django.contrib.contenttypes.models import ContentType
|
|
17
17
|
import hashlib
|
|
18
18
|
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import secrets
|
|
19
21
|
from io import BytesIO
|
|
20
22
|
from django.core.files.base import ContentFile
|
|
21
23
|
import qrcode
|
|
@@ -62,6 +64,34 @@ class SigilRoot(Entity):
|
|
|
62
64
|
verbose_name_plural = "Sigil Roots"
|
|
63
65
|
|
|
64
66
|
|
|
67
|
+
class Lead(models.Model):
|
|
68
|
+
"""Common request lead information."""
|
|
69
|
+
|
|
70
|
+
user = models.ForeignKey(
|
|
71
|
+
settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL
|
|
72
|
+
)
|
|
73
|
+
path = models.TextField(blank=True)
|
|
74
|
+
referer = models.TextField(blank=True)
|
|
75
|
+
user_agent = models.TextField(blank=True)
|
|
76
|
+
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
|
77
|
+
created_on = models.DateTimeField(auto_now_add=True)
|
|
78
|
+
|
|
79
|
+
class Meta:
|
|
80
|
+
abstract = True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InviteLead(Lead):
|
|
84
|
+
email = models.EmailField()
|
|
85
|
+
comment = models.TextField(blank=True)
|
|
86
|
+
|
|
87
|
+
class Meta:
|
|
88
|
+
verbose_name = "Invite Lead"
|
|
89
|
+
verbose_name_plural = "Invite Leads"
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
92
|
+
return self.email
|
|
93
|
+
|
|
94
|
+
|
|
65
95
|
class Address(Entity):
|
|
66
96
|
"""Physical location information for a user."""
|
|
67
97
|
|
|
@@ -1075,7 +1105,7 @@ class ReleaseManager(Entity):
|
|
|
1075
1105
|
max_length=200,
|
|
1076
1106
|
blank=True,
|
|
1077
1107
|
help_text=(
|
|
1078
|
-
"Personal access token
|
|
1108
|
+
"Personal access token for GitHub operations. "
|
|
1079
1109
|
"Used before the GITHUB_TOKEN environment variable."
|
|
1080
1110
|
),
|
|
1081
1111
|
)
|
|
@@ -1124,14 +1154,30 @@ class Package(Entity):
|
|
|
1124
1154
|
release_manager = models.ForeignKey(
|
|
1125
1155
|
ReleaseManager, on_delete=models.SET_NULL, null=True, blank=True
|
|
1126
1156
|
)
|
|
1157
|
+
is_active = models.BooleanField(
|
|
1158
|
+
default=False,
|
|
1159
|
+
help_text="Designates the active package for version comparisons",
|
|
1160
|
+
)
|
|
1127
1161
|
|
|
1128
1162
|
class Meta:
|
|
1129
1163
|
verbose_name = "Package"
|
|
1130
1164
|
verbose_name_plural = "Packages"
|
|
1165
|
+
constraints = [
|
|
1166
|
+
models.UniqueConstraint(
|
|
1167
|
+
fields=("is_active",),
|
|
1168
|
+
condition=models.Q(is_active=True),
|
|
1169
|
+
name="unique_active_package",
|
|
1170
|
+
)
|
|
1171
|
+
]
|
|
1131
1172
|
|
|
1132
1173
|
def __str__(self) -> str: # pragma: no cover - trivial
|
|
1133
1174
|
return self.name
|
|
1134
1175
|
|
|
1176
|
+
def save(self, *args, **kwargs):
|
|
1177
|
+
if self.is_active:
|
|
1178
|
+
type(self).objects.exclude(pk=self.pk).update(is_active=False)
|
|
1179
|
+
super().save(*args, **kwargs)
|
|
1180
|
+
|
|
1135
1181
|
def to_package(self) -> ReleasePackage:
|
|
1136
1182
|
"""Return a :class:`ReleasePackage` instance from package data."""
|
|
1137
1183
|
return ReleasePackage(
|
|
@@ -1159,7 +1205,6 @@ class PackageRelease(Entity):
|
|
|
1159
1205
|
max_length=40, blank=True, default=revision_utils.get_revision, editable=False
|
|
1160
1206
|
)
|
|
1161
1207
|
pypi_url = models.URLField("PyPI URL", blank=True, editable=False)
|
|
1162
|
-
pr_url = models.URLField("PR URL", blank=True, editable=False)
|
|
1163
1208
|
|
|
1164
1209
|
class Meta:
|
|
1165
1210
|
verbose_name = "Package Release"
|
|
@@ -1222,11 +1267,13 @@ class PackageRelease(Entity):
|
|
|
1222
1267
|
|
|
1223
1268
|
@property
|
|
1224
1269
|
def is_current(self) -> bool:
|
|
1225
|
-
"""Return ``True``
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1270
|
+
"""Return ``True`` when this release's version matches the VERSION file
|
|
1271
|
+
and its package is active."""
|
|
1272
|
+
version_path = Path("VERSION")
|
|
1273
|
+
if not version_path.exists():
|
|
1274
|
+
return False
|
|
1275
|
+
current_version = version_path.read_text().strip()
|
|
1276
|
+
return current_version == self.version and self.package.is_active
|
|
1230
1277
|
|
|
1231
1278
|
@classmethod
|
|
1232
1279
|
def latest(cls):
|
|
@@ -1251,17 +1298,29 @@ class PackageRelease(Entity):
|
|
|
1251
1298
|
)
|
|
1252
1299
|
self.revision = revision_utils.get_revision()
|
|
1253
1300
|
self.save(update_fields=["revision"])
|
|
1301
|
+
PackageRelease.dump_fixture()
|
|
1302
|
+
if kwargs.get("git"):
|
|
1303
|
+
diff = subprocess.run(
|
|
1304
|
+
["git", "status", "--porcelain", "core/fixtures/releases.json"],
|
|
1305
|
+
capture_output=True,
|
|
1306
|
+
text=True,
|
|
1307
|
+
)
|
|
1308
|
+
if diff.stdout.strip():
|
|
1309
|
+
release_utils._run(["git", "add", "core/fixtures/releases.json"])
|
|
1310
|
+
release_utils._run(
|
|
1311
|
+
[
|
|
1312
|
+
"git",
|
|
1313
|
+
"commit",
|
|
1314
|
+
"-m",
|
|
1315
|
+
f"chore: update release fixture for v{self.version}",
|
|
1316
|
+
]
|
|
1317
|
+
)
|
|
1318
|
+
release_utils._run(["git", "push"])
|
|
1254
1319
|
|
|
1255
1320
|
@property
|
|
1256
1321
|
def revision_short(self) -> str:
|
|
1257
1322
|
return self.revision[-6:] if self.revision else ""
|
|
1258
1323
|
|
|
1259
|
-
|
|
1260
|
-
@receiver([post_save, post_delete], sender=PackageRelease)
|
|
1261
|
-
def _update_release_fixture(sender, instance, **kwargs) -> None:
|
|
1262
|
-
"""Keep the release fixture in sync with the database."""
|
|
1263
|
-
PackageRelease.dump_fixture()
|
|
1264
|
-
|
|
1265
1324
|
# Ensure each RFID can only be linked to one energy account
|
|
1266
1325
|
@receiver(m2m_changed, sender=EnergyAccount.rfids.through)
|
|
1267
1326
|
def _rfid_unique_energy_account(sender, instance, action, reverse, model, pk_set, **kwargs):
|
|
@@ -1276,3 +1335,54 @@ def _rfid_unique_energy_account(sender, instance, action, reverse, model, pk_set
|
|
|
1276
1335
|
).exclude(energy_accounts=instance)
|
|
1277
1336
|
if conflict.exists():
|
|
1278
1337
|
raise ValidationError("RFID tags may only be assigned to one energy account.")
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def hash_key(key: str) -> str:
|
|
1341
|
+
"""Return a SHA-256 hash for ``key``."""
|
|
1342
|
+
|
|
1343
|
+
return hashlib.sha256(key.encode()).hexdigest()
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
class ChatProfile(models.Model):
|
|
1347
|
+
"""Stores a hashed user key used by the assistant for authentication.
|
|
1348
|
+
|
|
1349
|
+
The plain-text ``user_key`` is generated server-side and shown only once.
|
|
1350
|
+
Users must supply this key in the ``Authorization: Bearer <user_key>``
|
|
1351
|
+
header when requesting protected endpoints. Only the hash is stored.
|
|
1352
|
+
"""
|
|
1353
|
+
|
|
1354
|
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
1355
|
+
user = models.OneToOneField(
|
|
1356
|
+
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="chat_profile"
|
|
1357
|
+
)
|
|
1358
|
+
user_key_hash = models.CharField(max_length=64, unique=True)
|
|
1359
|
+
scopes = models.JSONField(default=list, blank=True)
|
|
1360
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
1361
|
+
last_used_at = models.DateTimeField(null=True, blank=True)
|
|
1362
|
+
is_active = models.BooleanField(default=True)
|
|
1363
|
+
|
|
1364
|
+
class Meta:
|
|
1365
|
+
db_table = "workgroup_chatprofile"
|
|
1366
|
+
verbose_name = "Chat Profile"
|
|
1367
|
+
verbose_name_plural = "Chat Profiles"
|
|
1368
|
+
|
|
1369
|
+
@classmethod
|
|
1370
|
+
def issue_key(cls, user) -> tuple["ChatProfile", str]:
|
|
1371
|
+
"""Create or update a profile and return it with a new plain key."""
|
|
1372
|
+
|
|
1373
|
+
key = secrets.token_hex(32)
|
|
1374
|
+
key_hash = hash_key(key)
|
|
1375
|
+
profile, _ = cls.objects.update_or_create(
|
|
1376
|
+
user=user,
|
|
1377
|
+
defaults={"user_key_hash": key_hash, "last_used_at": None, "is_active": True},
|
|
1378
|
+
)
|
|
1379
|
+
return profile, key
|
|
1380
|
+
|
|
1381
|
+
def touch(self) -> None:
|
|
1382
|
+
"""Record that the key was used."""
|
|
1383
|
+
|
|
1384
|
+
self.last_used_at = timezone.now()
|
|
1385
|
+
self.save(update_fields=["last_used_at"])
|
|
1386
|
+
|
|
1387
|
+
def __str__(self) -> str: # pragma: no cover - simple representation
|
|
1388
|
+
return f"ChatProfile for {self.user}"
|
core/release.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import subprocess
|
|
5
5
|
import sys
|
|
6
|
+
import shutil
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Optional
|
|
@@ -82,20 +83,12 @@ def _git_clean() -> bool:
|
|
|
82
83
|
return not proc.stdout.strip()
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
def
|
|
86
|
-
|
|
86
|
+
def _git_has_staged_changes() -> bool:
|
|
87
|
+
"""Return True if there are staged changes ready to commit."""
|
|
88
|
+
proc = subprocess.run(["git", "diff", "--cached", "--quiet"])
|
|
89
|
+
return proc.returncode != 0
|
|
87
90
|
|
|
88
91
|
|
|
89
|
-
def _current_branch() -> str:
|
|
90
|
-
return (
|
|
91
|
-
subprocess.check_output([
|
|
92
|
-
"git",
|
|
93
|
-
"rev-parse",
|
|
94
|
-
"--abbrev-ref",
|
|
95
|
-
"HEAD",
|
|
96
|
-
]).decode().strip()
|
|
97
|
-
)
|
|
98
|
-
|
|
99
92
|
|
|
100
93
|
def _manager_credentials() -> Optional[Credentials]:
|
|
101
94
|
"""Return credentials from the Package's release manager if available."""
|
|
@@ -174,69 +167,6 @@ def _write_pyproject(package: Package, version: str, requirements: list[str]) ->
|
|
|
174
167
|
Path("pyproject.toml").write_text(_dump_toml(content), encoding="utf-8")
|
|
175
168
|
|
|
176
169
|
|
|
177
|
-
def _ensure_changelog() -> str:
|
|
178
|
-
header = "Changelog\n=========\n\n"
|
|
179
|
-
path = Path("CHANGELOG.rst")
|
|
180
|
-
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
181
|
-
if not text.startswith("Changelog"):
|
|
182
|
-
text = header + text
|
|
183
|
-
if "Unreleased" not in text:
|
|
184
|
-
text = text[: len(header)] + "Unreleased\n----------\n\n" + text[len(header):]
|
|
185
|
-
return text
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def _pop_unreleased(text: str) -> tuple[str, str]:
|
|
189
|
-
lines = text.splitlines()
|
|
190
|
-
try:
|
|
191
|
-
idx = lines.index("Unreleased")
|
|
192
|
-
except ValueError:
|
|
193
|
-
return "", text
|
|
194
|
-
body = []
|
|
195
|
-
i = idx + 2
|
|
196
|
-
while i < len(lines) and lines[i].startswith("- "):
|
|
197
|
-
body.append(lines[i])
|
|
198
|
-
i += 1
|
|
199
|
-
if i < len(lines) and lines[i] == "":
|
|
200
|
-
i += 1
|
|
201
|
-
new_lines = lines[:idx] + lines[i:]
|
|
202
|
-
return "\n".join(body), "\n".join(new_lines) + ("\n" if text.endswith("\n") else "")
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def _last_changelog_revision() -> Optional[str]:
|
|
206
|
-
path = Path("CHANGELOG.rst")
|
|
207
|
-
if not path.exists():
|
|
208
|
-
return None
|
|
209
|
-
for line in path.read_text(encoding="utf-8").splitlines():
|
|
210
|
-
if "[revision" in line:
|
|
211
|
-
try:
|
|
212
|
-
return line.split("[revision", 1)[1].split("]", 1)[0].strip()
|
|
213
|
-
except Exception:
|
|
214
|
-
return None
|
|
215
|
-
return None
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def update_changelog(version: str, revision: str, prev_revision: Optional[str] = None) -> None:
|
|
219
|
-
text = _ensure_changelog()
|
|
220
|
-
body, text = _pop_unreleased(text)
|
|
221
|
-
if not body:
|
|
222
|
-
prev_revision = prev_revision or _last_changelog_revision()
|
|
223
|
-
log_range = f"{prev_revision}..HEAD" if prev_revision else "HEAD"
|
|
224
|
-
proc = subprocess.run(
|
|
225
|
-
["git", "log", "--pretty=%h %s", "--no-merges", log_range],
|
|
226
|
-
capture_output=True,
|
|
227
|
-
text=True,
|
|
228
|
-
check=False,
|
|
229
|
-
)
|
|
230
|
-
body = "\n".join(
|
|
231
|
-
f"- {l.strip()}" for l in proc.stdout.splitlines() if l.strip()
|
|
232
|
-
)
|
|
233
|
-
header = f"{version} [revision {revision}]"
|
|
234
|
-
underline = "-" * len(header)
|
|
235
|
-
entry = "\n".join([header, underline, "", body, ""]).rstrip() + "\n\n"
|
|
236
|
-
base_header = "Changelog\n=========\n\n"
|
|
237
|
-
remaining = text[len(base_header):]
|
|
238
|
-
new_text = base_header + "Unreleased\n----------\n\n" + entry + remaining
|
|
239
|
-
Path("CHANGELOG.rst").write_text(new_text, encoding="utf-8")
|
|
240
170
|
|
|
241
171
|
|
|
242
172
|
@requires_network
|
|
@@ -268,8 +198,8 @@ def build(
|
|
|
268
198
|
"Git repository is not clean. Commit, stash, or enable auto stash before building."
|
|
269
199
|
)
|
|
270
200
|
|
|
201
|
+
version_path = Path("VERSION")
|
|
271
202
|
if version is None:
|
|
272
|
-
version_path = Path("VERSION")
|
|
273
203
|
if not version_path.exists():
|
|
274
204
|
raise ReleaseError("VERSION file not found")
|
|
275
205
|
version = version_path.read_text().strip()
|
|
@@ -278,6 +208,9 @@ def build(
|
|
|
278
208
|
patch += 1
|
|
279
209
|
version = f"{major}.{minor}.{patch}"
|
|
280
210
|
version_path.write_text(version + "\n")
|
|
211
|
+
else:
|
|
212
|
+
# Ensure the VERSION file reflects the provided release version
|
|
213
|
+
version_path.write_text(version + "\n")
|
|
281
214
|
|
|
282
215
|
requirements = [
|
|
283
216
|
line.strip()
|
|
@@ -291,16 +224,10 @@ def build(
|
|
|
291
224
|
if proc.returncode != 0:
|
|
292
225
|
raise TestsFailed(log_path, proc.stdout + proc.stderr)
|
|
293
226
|
|
|
294
|
-
commit_hash = _current_commit()
|
|
295
|
-
prev_revision = _last_changelog_revision()
|
|
296
|
-
update_changelog(version, commit_hash, prev_revision)
|
|
297
|
-
|
|
298
227
|
_write_pyproject(package, version, requirements)
|
|
299
228
|
if dist:
|
|
300
229
|
if Path("dist").exists():
|
|
301
|
-
|
|
302
|
-
p.unlink()
|
|
303
|
-
Path("dist").rmdir()
|
|
230
|
+
shutil.rmtree("dist")
|
|
304
231
|
try:
|
|
305
232
|
import build # type: ignore
|
|
306
233
|
except Exception:
|
|
@@ -308,10 +235,11 @@ def build(
|
|
|
308
235
|
_run([sys.executable, "-m", "build"])
|
|
309
236
|
|
|
310
237
|
if git:
|
|
311
|
-
files = ["VERSION", "pyproject.toml"
|
|
238
|
+
files = ["VERSION", "pyproject.toml"]
|
|
312
239
|
_run(["git", "add"] + files)
|
|
313
240
|
msg = f"PyPI Release v{version}" if twine else f"Release v{version}"
|
|
314
|
-
|
|
241
|
+
if _git_has_staged_changes():
|
|
242
|
+
_run(["git", "commit", "-m", msg])
|
|
315
243
|
_run(["git", "push"])
|
|
316
244
|
|
|
317
245
|
if tag:
|
|
@@ -359,63 +287,23 @@ def promote(
|
|
|
359
287
|
package: Package = DEFAULT_PACKAGE,
|
|
360
288
|
version: str,
|
|
361
289
|
creds: Optional[Credentials] = None,
|
|
362
|
-
) ->
|
|
363
|
-
"""
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
stashed = True
|
|
379
|
-
build(
|
|
380
|
-
package=package,
|
|
381
|
-
version=version,
|
|
382
|
-
creds=creds,
|
|
383
|
-
tests=False,
|
|
384
|
-
dist=True,
|
|
385
|
-
git=False,
|
|
386
|
-
tag=False,
|
|
387
|
-
stash=True,
|
|
388
|
-
)
|
|
389
|
-
try: # best effort
|
|
390
|
-
_run(
|
|
391
|
-
[
|
|
392
|
-
sys.executable,
|
|
393
|
-
"manage.py",
|
|
394
|
-
"squashmigrations",
|
|
395
|
-
"core",
|
|
396
|
-
"0001",
|
|
397
|
-
"--noinput",
|
|
398
|
-
],
|
|
399
|
-
check=False,
|
|
400
|
-
)
|
|
401
|
-
except Exception:
|
|
402
|
-
# The squashmigrations command may not be available or could fail
|
|
403
|
-
# (e.g. when no migrations exist). Any errors should not interrupt
|
|
404
|
-
# the release promotion flow.
|
|
405
|
-
pass
|
|
406
|
-
_run(["git", "add", "."]) # add all changes
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Build the package and commit the release on the current branch."""
|
|
292
|
+
if not _git_clean():
|
|
293
|
+
raise ReleaseError("Git repository is not clean")
|
|
294
|
+
build(
|
|
295
|
+
package=package,
|
|
296
|
+
version=version,
|
|
297
|
+
creds=creds,
|
|
298
|
+
tests=False,
|
|
299
|
+
dist=True,
|
|
300
|
+
git=False,
|
|
301
|
+
tag=False,
|
|
302
|
+
stash=False,
|
|
303
|
+
)
|
|
304
|
+
_run(["git", "add", "."]) # add all changes
|
|
305
|
+
if _git_has_staged_changes():
|
|
407
306
|
_run(["git", "commit", "-m", f"Release v{version}"])
|
|
408
|
-
commit_hash = _current_commit()
|
|
409
|
-
release_name = f"{package.name}-{version}-{commit_hash[:7]}"
|
|
410
|
-
branch = f"release-{release_name}"
|
|
411
|
-
_run(["git", "branch", "-m", branch])
|
|
412
|
-
except Exception:
|
|
413
|
-
_run(["git", "checkout", current])
|
|
414
|
-
raise
|
|
415
|
-
finally:
|
|
416
|
-
if stashed:
|
|
417
|
-
_run(["git", "stash", "pop"], check=False)
|
|
418
|
-
return commit_hash, branch, current
|
|
419
307
|
|
|
420
308
|
|
|
421
309
|
def publish(
|
core/system.py
CHANGED
|
@@ -31,8 +31,8 @@ def _gather_info() -> dict:
|
|
|
31
31
|
info["mode"] = mode
|
|
32
32
|
info["port"] = 8000 if mode == "public" else 8888
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
info["role"] =
|
|
34
|
+
# Use settings.NODE_ROLE as the single source of truth for the node role.
|
|
35
|
+
info["role"] = getattr(settings, "NODE_ROLE", "Terminal")
|
|
36
36
|
|
|
37
37
|
info["features"] = {
|
|
38
38
|
"celery": (lock_dir / "celery.lck").exists(),
|
core/test_system_info.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
4
|
+
|
|
5
|
+
import django
|
|
6
|
+
django.setup()
|
|
7
|
+
|
|
8
|
+
from django.test import SimpleTestCase, override_settings
|
|
9
|
+
from core.system import _gather_info
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SystemInfoRoleTests(SimpleTestCase):
|
|
13
|
+
@override_settings(NODE_ROLE="Terminal")
|
|
14
|
+
def test_defaults_to_terminal(self):
|
|
15
|
+
info = _gather_info()
|
|
16
|
+
self.assertEqual(info["role"], "Terminal")
|
|
17
|
+
|
|
18
|
+
@override_settings(NODE_ROLE="Satellite")
|
|
19
|
+
def test_uses_settings_role(self):
|
|
20
|
+
info = _gather_info()
|
|
21
|
+
self.assertEqual(info["role"], "Satellite")
|