arthexis 0.1.16__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 (63) hide show
  1. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/METADATA +84 -35
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. config/asgi.py +1 -15
  4. config/middleware.py +47 -1
  5. config/settings.py +15 -30
  6. config/urls.py +53 -1
  7. core/admin.py +540 -450
  8. core/apps.py +0 -6
  9. core/auto_upgrade.py +19 -4
  10. core/backends.py +13 -3
  11. core/changelog.py +66 -5
  12. core/environment.py +4 -5
  13. core/models.py +1566 -203
  14. core/notifications.py +1 -1
  15. core/reference_utils.py +10 -11
  16. core/release.py +55 -7
  17. core/sigil_builder.py +2 -2
  18. core/sigil_resolver.py +1 -66
  19. core/system.py +268 -2
  20. core/tasks.py +174 -48
  21. core/tests.py +314 -16
  22. core/user_data.py +42 -2
  23. core/views.py +278 -183
  24. nodes/admin.py +557 -65
  25. nodes/apps.py +11 -0
  26. nodes/models.py +658 -113
  27. nodes/rfid_sync.py +1 -1
  28. nodes/tasks.py +97 -2
  29. nodes/tests.py +1212 -116
  30. nodes/urls.py +15 -1
  31. nodes/utils.py +51 -3
  32. nodes/views.py +1239 -154
  33. ocpp/admin.py +979 -152
  34. ocpp/consumers.py +268 -28
  35. ocpp/models.py +488 -3
  36. ocpp/network.py +398 -0
  37. ocpp/store.py +6 -4
  38. ocpp/tasks.py +296 -2
  39. ocpp/test_export_import.py +1 -0
  40. ocpp/test_rfid.py +121 -4
  41. ocpp/tests.py +950 -11
  42. ocpp/transactions_io.py +9 -1
  43. ocpp/urls.py +3 -3
  44. ocpp/views.py +596 -51
  45. pages/admin.py +262 -30
  46. pages/apps.py +35 -0
  47. pages/context_processors.py +26 -21
  48. pages/defaults.py +1 -1
  49. pages/forms.py +31 -8
  50. pages/middleware.py +6 -2
  51. pages/models.py +77 -2
  52. pages/module_defaults.py +5 -5
  53. pages/site_config.py +137 -0
  54. pages/tests.py +885 -109
  55. pages/urls.py +13 -2
  56. pages/utils.py +70 -0
  57. pages/views.py +558 -55
  58. arthexis-0.1.16.dist-info/RECORD +0 -111
  59. core/workgroup_urls.py +0 -17
  60. core/workgroup_views.py +0 -94
  61. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  62. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +0 -0
  63. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
core/apps.py CHANGED
@@ -348,9 +348,3 @@ class CoreConfig(AppConfig):
348
348
  weak=False,
349
349
  )
350
350
 
351
- try:
352
- from .mcp.auto_start import schedule_auto_start
353
-
354
- schedule_auto_start(check_profiles_immediately=False)
355
- except Exception: # pragma: no cover - defensive
356
- logger.exception("Failed to schedule MCP auto-start")
core/auto_upgrade.py CHANGED
@@ -10,6 +10,14 @@ from django.conf import settings
10
10
  AUTO_UPGRADE_TASK_NAME = "auto-upgrade-check"
11
11
  AUTO_UPGRADE_TASK_PATH = "core.tasks.check_github_updates"
12
12
 
13
+ DEFAULT_AUTO_UPGRADE_MODE = "version"
14
+ AUTO_UPGRADE_INTERVAL_MINUTES = {
15
+ "latest": 5,
16
+ "stable": 60,
17
+ DEFAULT_AUTO_UPGRADE_MODE: 720,
18
+ }
19
+ AUTO_UPGRADE_FALLBACK_INTERVAL = AUTO_UPGRADE_INTERVAL_MINUTES["stable"]
20
+
13
21
 
14
22
  def ensure_auto_upgrade_periodic_task(
15
23
  sender=None, *, base_dir: Path | None = None, **kwargs
@@ -30,8 +38,6 @@ def ensure_auto_upgrade_periodic_task(
30
38
 
31
39
  lock_dir = base_dir / "locks"
32
40
  mode_file = lock_dir / "auto_upgrade.lck"
33
- if not mode_file.exists():
34
- return
35
41
 
36
42
  try: # pragma: no cover - optional dependency failures
37
43
  from django_celery_beat.models import IntervalSchedule, PeriodicTask
@@ -39,8 +45,17 @@ def ensure_auto_upgrade_periodic_task(
39
45
  except Exception:
40
46
  return
41
47
 
42
- _mode = mode_file.read_text().strip() or "version"
43
- interval_minutes = 5
48
+ if not mode_file.exists():
49
+ try:
50
+ PeriodicTask.objects.filter(name=AUTO_UPGRADE_TASK_NAME).delete()
51
+ except (OperationalError, ProgrammingError): # pragma: no cover - DB not ready
52
+ return
53
+ return
54
+
55
+ _mode = mode_file.read_text().strip().lower() or DEFAULT_AUTO_UPGRADE_MODE
56
+ interval_minutes = AUTO_UPGRADE_INTERVAL_MINUTES.get(
57
+ _mode, AUTO_UPGRADE_FALLBACK_INTERVAL
58
+ )
44
59
 
45
60
  try:
46
61
  schedule, _ = IntervalSchedule.objects.get_or_create(
core/backends.py CHANGED
@@ -81,15 +81,22 @@ class RFIDBackend:
81
81
  if not rfid_value:
82
82
  return None
83
83
 
84
- tag = RFID.objects.filter(rfid=rfid_value).first()
85
- if not tag or not tag.allowed:
84
+ tag = RFID.matching_queryset(rfid_value).filter(allowed=True).first()
85
+ if not tag:
86
86
  return None
87
87
 
88
+ update_fields: list[str] = []
89
+ if tag.adopt_rfid(rfid_value):
90
+ update_fields.append("rfid")
91
+ if update_fields:
92
+ tag.save(update_fields=update_fields)
93
+
88
94
  command = (tag.external_command or "").strip()
89
95
  if command:
90
96
  env = os.environ.copy()
91
97
  env["RFID_VALUE"] = rfid_value
92
98
  env["RFID_LABEL_ID"] = str(tag.pk)
99
+ env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
93
100
  try:
94
101
  completed = subprocess.run(
95
102
  command,
@@ -117,6 +124,7 @@ class RFIDBackend:
117
124
  env = os.environ.copy()
118
125
  env["RFID_VALUE"] = rfid_value
119
126
  env["RFID_LABEL_ID"] = str(tag.pk)
127
+ env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
120
128
  with contextlib.suppress(Exception):
121
129
  subprocess.Popen(
122
130
  post_command,
@@ -209,7 +217,9 @@ class LocalhostAdminBackend(ModelBackend):
209
217
  try:
210
218
  ipaddress.ip_address(host)
211
219
  except ValueError:
212
- if not self._is_test_environment(request):
220
+ if host.lower() == "localhost":
221
+ host = "127.0.0.1"
222
+ elif not self._is_test_environment(request):
213
223
  return None
214
224
  forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
215
225
  if forwarded:
core/changelog.py CHANGED
@@ -154,9 +154,53 @@ def _parse_sections(text: str) -> List[ChangelogSection]:
154
154
  return sections
155
155
 
156
156
 
157
+ def _latest_release_version(previous_text: str) -> Optional[str]:
158
+ for section in _parse_sections(previous_text):
159
+ if section.version:
160
+ return section.version
161
+ return None
162
+
163
+
164
+ def _find_release_commit(version: str) -> Optional[str]:
165
+ normalized = version.lstrip("v")
166
+ search_terms = [
167
+ f"Release v{normalized}",
168
+ f"Release {normalized}",
169
+ f"pre-release commit v{normalized}",
170
+ f"pre-release commit {normalized}",
171
+ ]
172
+ for term in search_terms:
173
+ proc = subprocess.run(
174
+ [
175
+ "git",
176
+ "log",
177
+ "--max-count=1",
178
+ "--format=%H",
179
+ "--fixed-strings",
180
+ f"--grep={term}",
181
+ ],
182
+ capture_output=True,
183
+ text=True,
184
+ check=False,
185
+ )
186
+ sha = proc.stdout.strip()
187
+ if sha:
188
+ return sha.splitlines()[0]
189
+ return None
190
+
191
+
192
+ def _resolve_release_commit_from_text(previous_text: str) -> Optional[str]:
193
+ version = _latest_release_version(previous_text)
194
+ if not version:
195
+ return None
196
+ return _find_release_commit(version)
197
+
198
+
157
199
  def _merge_sections(
158
200
  new_sections: Iterable[ChangelogSection],
159
201
  old_sections: Iterable[ChangelogSection],
202
+ *,
203
+ reopen_latest: bool = False,
160
204
  ) -> List[ChangelogSection]:
161
205
  merged = list(new_sections)
162
206
  old_sections_list = list(old_sections)
@@ -199,7 +243,8 @@ def _merge_sections(
199
243
  existing = version_to_section.get(old.version)
200
244
  if existing is None:
201
245
  if (
202
- first_release_version
246
+ reopen_latest
247
+ and first_release_version
203
248
  and old.version == first_release_version
204
249
  and not reopened_latest_version
205
250
  and unreleased_section is not None
@@ -274,29 +319,45 @@ def _resolve_start_tag(explicit: str | None = None) -> Optional[str]:
274
319
  return None
275
320
 
276
321
 
277
- def determine_range_spec(start_tag: str | None = None) -> str:
322
+ def determine_range_spec(
323
+ start_tag: str | None = None, *, previous_text: str | None = None
324
+ ) -> str:
278
325
  """Return the git range specification to build the changelog."""
279
326
 
280
327
  resolved = _resolve_start_tag(start_tag)
281
328
  if resolved:
282
329
  return f"{resolved}..HEAD"
330
+
331
+ if previous_text:
332
+ release_commit = _resolve_release_commit_from_text(previous_text)
333
+ if release_commit:
334
+ return f"{release_commit}..HEAD"
335
+
283
336
  return "HEAD"
284
337
 
285
338
 
286
339
  def collect_sections(
287
- *, range_spec: str = "HEAD", previous_text: str | None = None
340
+ *,
341
+ range_spec: str = "HEAD",
342
+ previous_text: str | None = None,
343
+ reopen_latest: bool = False,
288
344
  ) -> List[ChangelogSection]:
289
345
  """Return changelog sections for *range_spec*.
290
346
 
291
347
  When ``previous_text`` is provided, sections not regenerated in the current run
292
- are appended so long as they can be parsed from the existing changelog.
348
+ are appended so long as they can be parsed from the existing changelog. Set
349
+ ``reopen_latest`` to ``True`` when the caller intends to move the most recent
350
+ release notes back into the ``Unreleased`` section (for example, when
351
+ preparing a release retry before a new tag is created).
293
352
  """
294
353
 
295
354
  commits = _read_commits(range_spec)
296
355
  sections = _sections_from_commits(commits)
297
356
  if previous_text:
298
357
  old_sections = _parse_sections(previous_text)
299
- sections = _merge_sections(sections, old_sections)
358
+ sections = _merge_sections(
359
+ sections, old_sections, reopen_latest=reopen_latest
360
+ )
300
361
  return sections
301
362
 
302
363
 
core/environment.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
-
5
4
  from django.conf import settings
6
5
  from django.contrib import admin
7
6
  from django.template.response import TemplateResponse
@@ -20,18 +19,18 @@ def _environment_view(request):
20
19
  context = admin.site.each_context(request)
21
20
  context.update(
22
21
  {
23
- "title": _("Environ"),
22
+ "title": _("Environment"),
24
23
  "env_vars": env_vars,
24
+ "environment_tasks": [],
25
25
  }
26
26
  )
27
27
  return TemplateResponse(request, "admin/environment.html", context)
28
28
 
29
-
30
29
  def _config_view(request):
31
30
  context = admin.site.each_context(request)
32
31
  context.update(
33
32
  {
34
- "title": _("Config"),
33
+ "title": _("Django Config"),
35
34
  "django_settings": _get_django_settings(),
36
35
  }
37
36
  )
@@ -39,7 +38,7 @@ def _config_view(request):
39
38
 
40
39
 
41
40
  def patch_admin_environment_view() -> None:
42
- """Add custom admin view for environment information."""
41
+ """Register the Environment and Config admin views on the main admin site."""
43
42
  original_get_urls = admin.site.get_urls
44
43
 
45
44
  def get_urls():