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
@@ -0,0 +1,113 @@
1
+ """Shared helpers for RFID import and export workflows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import OrderedDict
6
+ from collections.abc import Iterable, Mapping
7
+
8
+ from core.models import EnergyAccount, RFID
9
+
10
+
11
+ def account_column_for_field(account_field: str) -> str:
12
+ """Return the column name that should be used for the account field.
13
+
14
+ Args:
15
+ account_field: Either ``"id"`` or ``"name"`` depending on how energy
16
+ accounts should be represented.
17
+
18
+ Returns:
19
+ The CSV column header to use for the selected account field.
20
+ """
21
+
22
+ return "energy_account_names" if account_field == "name" else "energy_accounts"
23
+
24
+
25
+ def serialize_accounts(tag: RFID, account_field: str) -> str:
26
+ """Convert the RFID's accounts to a serialized string."""
27
+
28
+ accounts = tag.energy_accounts.all()
29
+ if account_field == "name":
30
+ return ",".join(account.name for account in accounts if account.name)
31
+ return ",".join(str(account.id) for account in accounts)
32
+
33
+
34
+ def _normalized_unique_names(values: Iterable[str]) -> list[str]:
35
+ """Return a list of unique, normalized account names preserving order."""
36
+
37
+ seen: OrderedDict[str, None] = OrderedDict()
38
+ for value in values:
39
+ normalized = value.strip().upper()
40
+ if not normalized:
41
+ continue
42
+ if normalized not in seen:
43
+ seen[normalized] = None
44
+ return list(seen.keys())
45
+
46
+
47
+ def _accounts_from_ids(values: Iterable[str]) -> list[EnergyAccount]:
48
+ """Resolve a list of account ids into EnergyAccount instances."""
49
+
50
+ identifiers: list[int] = []
51
+ for value in values:
52
+ value = value.strip()
53
+ if not value:
54
+ continue
55
+ try:
56
+ identifiers.append(int(value))
57
+ except (TypeError, ValueError):
58
+ continue
59
+ if not identifiers:
60
+ return []
61
+ existing = EnergyAccount.objects.in_bulk(identifiers)
62
+ return [existing[idx] for idx in identifiers if idx in existing]
63
+
64
+
65
+ def parse_accounts(row: Mapping[str, object], account_field: str) -> list[EnergyAccount]:
66
+ """Resolve energy accounts for an RFID import row.
67
+
68
+ Args:
69
+ row: Mapping of column names to raw values for the import row.
70
+ account_field: Preferred field (``"id"`` or ``"name"``) describing how
71
+ accounts are encoded.
72
+
73
+ Returns:
74
+ A list of :class:`EnergyAccount` instances. The list will be empty when
75
+ no accounts should be linked.
76
+ """
77
+
78
+ preferred_column = account_column_for_field(account_field)
79
+ fallback_column = (
80
+ "energy_accounts"
81
+ if preferred_column == "energy_account_names"
82
+ else "energy_account_names"
83
+ )
84
+
85
+ def _value_for(column: str) -> str:
86
+ raw = row.get(column, "")
87
+ if raw is None:
88
+ return ""
89
+ return str(raw).strip()
90
+
91
+ raw_value = _value_for(preferred_column)
92
+ effective_field = account_field
93
+
94
+ if not raw_value:
95
+ raw_value = _value_for(fallback_column)
96
+ if raw_value:
97
+ effective_field = (
98
+ "name" if fallback_column == "energy_account_names" else "id"
99
+ )
100
+
101
+ if not raw_value:
102
+ return []
103
+
104
+ parts = raw_value.split(",")
105
+ if effective_field == "name":
106
+ accounts: list[EnergyAccount] = []
107
+ for normalized_name in _normalized_unique_names(parts):
108
+ account, _ = EnergyAccount.objects.get_or_create(name=normalized_name)
109
+ accounts.append(account)
110
+ return accounts
111
+
112
+ return _accounts_from_ids(parts)
113
+
core/sigil_builder.py CHANGED
@@ -1,149 +1,149 @@
1
- from __future__ import annotations
2
-
3
-
4
- from django.apps import apps
5
- from django.contrib import admin
6
- from django.template.response import TemplateResponse
7
- from django.urls import path, reverse
8
- from django.utils.translation import gettext_lazy as _
9
-
10
- from .fields import SigilAutoFieldMixin
11
- from .sigil_resolver import (
12
- resolve_sigils as resolve_sigils_in_text,
13
- resolve_sigil as _resolve_sigil,
14
- )
15
-
16
-
17
- def generate_model_sigils(**kwargs) -> None:
18
- """Ensure built-in configuration SigilRoot entries exist."""
19
- SigilRoot = apps.get_model("core", "SigilRoot")
20
- for prefix in ["ENV", "CONF", "SYS"]:
21
- # Ensure built-in configuration roots exist without violating the
22
- # unique ``prefix`` constraint, even if older databases already have
23
- # entries with a different ``context_type``.
24
- root = SigilRoot.objects.filter(prefix__iexact=prefix).first()
25
- if root:
26
- root.prefix = prefix
27
- root.context_type = SigilRoot.Context.CONFIG
28
- root.save(update_fields=["prefix", "context_type"])
29
- else:
30
- SigilRoot.objects.create(
31
- prefix=prefix,
32
- context_type=SigilRoot.Context.CONFIG,
33
- )
34
-
35
-
36
- def _sigil_builder_view(request):
37
- SigilRoot = apps.get_model("core", "SigilRoot")
38
- grouped: dict[str, dict[str, object]] = {}
39
- builtin_roots = [
40
- {
41
- "prefix": "ENV",
42
- "url": reverse("admin:environment"),
43
- "label": _("Environ"),
44
- },
45
- {
46
- "prefix": "CONF",
47
- "url": reverse("admin:config"),
48
- "label": _("Config"),
49
- },
50
- {
51
- "prefix": "SYS",
52
- "url": reverse("admin:system"),
53
- "label": _("System"),
54
- },
55
- ]
56
- for root in SigilRoot.objects.filter(
57
- context_type=SigilRoot.Context.ENTITY
58
- ).select_related("content_type"):
59
- if not root.content_type:
60
- continue
61
- model = root.content_type.model_class()
62
- model_name = model._meta.object_name
63
- entry = grouped.setdefault(
64
- model_name,
65
- {
66
- "model": model_name,
67
- "fields": [f.name.upper() for f in model._meta.fields],
68
- "prefixes": [],
69
- },
70
- )
71
- entry["prefixes"].append(root.prefix.upper())
72
- roots = sorted(grouped.values(), key=lambda r: r["model"])
73
- for entry in roots:
74
- entry["prefixes"].sort()
75
-
76
- auto_fields = []
77
- seen = set()
78
- for model in apps.get_models():
79
- model_name = model._meta.object_name
80
- if model_name in seen:
81
- continue
82
- seen.add(model_name)
83
- prefixes = grouped.get(model_name, {}).get("prefixes", [])
84
- for field in model._meta.fields:
85
- if isinstance(field, SigilAutoFieldMixin):
86
- auto_fields.append(
87
- {
88
- "model": model_name,
89
- "roots": prefixes,
90
- "field": field.name.upper(),
91
- }
92
- )
93
-
94
- sigils_text = ""
95
- resolved_text = ""
96
- show_sigils_input = True
97
- show_result = False
98
- if request.method == "POST":
99
- sigils_text = request.POST.get("sigils_text", "")
100
- source_text = sigils_text
101
- upload = request.FILES.get("sigils_file")
102
- if upload:
103
- source_text = upload.read().decode("utf-8", errors="ignore")
104
- show_sigils_input = False
105
- else:
106
- single = request.POST.get("sigil", "")
107
- if single:
108
- source_text = (
109
- f"[{single}]" if not single.startswith("[") else single
110
- )
111
- sigils_text = source_text
112
- if source_text:
113
- resolved_text = resolve_sigils_in_text(source_text)
114
- show_result = True
115
- if upload:
116
- sigils_text = ""
117
-
118
- context = admin.site.each_context(request)
119
- context.update(
120
- {
121
- "title": _("Sigil Builder"),
122
- "sigil_roots": roots,
123
- "builtin_roots": builtin_roots,
124
- "auto_fields": auto_fields,
125
- "sigils_text": sigils_text,
126
- "resolved_text": resolved_text,
127
- "show_sigils_input": show_sigils_input,
128
- "show_result": show_result,
129
- }
130
- )
131
- return TemplateResponse(request, "admin/sigil_builder.html", context)
132
-
133
-
134
- def patch_admin_sigil_builder_view() -> None:
135
- """Add custom admin view for listing SigilRoots."""
136
- original_get_urls = admin.site.get_urls
137
-
138
- def get_urls():
139
- urls = original_get_urls()
140
- custom = [
141
- path(
142
- "sigil-builder/",
143
- admin.site.admin_view(_sigil_builder_view),
144
- name="sigil_builder",
145
- ),
146
- ]
147
- return custom + urls
148
-
149
- admin.site.get_urls = get_urls
1
+ from __future__ import annotations
2
+
3
+
4
+ from django.apps import apps
5
+ from django.contrib import admin
6
+ from django.template.response import TemplateResponse
7
+ from django.urls import path, reverse
8
+ from django.utils.translation import gettext_lazy as _
9
+
10
+ from .fields import SigilAutoFieldMixin
11
+ from .sigil_resolver import (
12
+ resolve_sigils as resolve_sigils_in_text,
13
+ resolve_sigil as _resolve_sigil,
14
+ )
15
+
16
+
17
+ def generate_model_sigils(**kwargs) -> None:
18
+ """Ensure built-in configuration SigilRoot entries exist."""
19
+ SigilRoot = apps.get_model("core", "SigilRoot")
20
+ for prefix in ["ENV", "CONF", "SYS"]:
21
+ # Ensure built-in configuration roots exist without violating the
22
+ # unique ``prefix`` constraint, even if older databases already have
23
+ # entries with a different ``context_type``.
24
+ root = SigilRoot.objects.filter(prefix__iexact=prefix).first()
25
+ if root:
26
+ root.prefix = prefix
27
+ root.context_type = SigilRoot.Context.CONFIG
28
+ root.save(update_fields=["prefix", "context_type"])
29
+ else:
30
+ SigilRoot.objects.create(
31
+ prefix=prefix,
32
+ context_type=SigilRoot.Context.CONFIG,
33
+ )
34
+
35
+
36
+ def _sigil_builder_view(request):
37
+ SigilRoot = apps.get_model("core", "SigilRoot")
38
+ grouped: dict[str, dict[str, object]] = {}
39
+ builtin_roots = [
40
+ {
41
+ "prefix": "ENV",
42
+ "url": reverse("admin:environment"),
43
+ "label": _("Environ"),
44
+ },
45
+ {
46
+ "prefix": "CONF",
47
+ "url": reverse("admin:config"),
48
+ "label": _("Config"),
49
+ },
50
+ {
51
+ "prefix": "SYS",
52
+ "url": reverse("admin:system"),
53
+ "label": _("System"),
54
+ },
55
+ ]
56
+ for root in SigilRoot.objects.filter(
57
+ context_type=SigilRoot.Context.ENTITY
58
+ ).select_related("content_type"):
59
+ if not root.content_type:
60
+ continue
61
+ model = root.content_type.model_class()
62
+ model_name = model._meta.object_name
63
+ entry = grouped.setdefault(
64
+ model_name,
65
+ {
66
+ "model": model_name,
67
+ "fields": [f.name.upper() for f in model._meta.fields],
68
+ "prefixes": [],
69
+ },
70
+ )
71
+ entry["prefixes"].append(root.prefix.upper())
72
+ roots = sorted(grouped.values(), key=lambda r: r["model"])
73
+ for entry in roots:
74
+ entry["prefixes"].sort()
75
+
76
+ auto_fields = []
77
+ seen = set()
78
+ for model in apps.get_models():
79
+ model_name = model._meta.object_name
80
+ if model_name in seen:
81
+ continue
82
+ seen.add(model_name)
83
+ prefixes = grouped.get(model_name, {}).get("prefixes", [])
84
+ for field in model._meta.fields:
85
+ if isinstance(field, SigilAutoFieldMixin):
86
+ auto_fields.append(
87
+ {
88
+ "model": model_name,
89
+ "roots": prefixes,
90
+ "field": field.name.upper(),
91
+ }
92
+ )
93
+
94
+ sigils_text = ""
95
+ resolved_text = ""
96
+ show_sigils_input = True
97
+ show_result = False
98
+ if request.method == "POST":
99
+ sigils_text = request.POST.get("sigils_text", "")
100
+ source_text = sigils_text
101
+ upload = request.FILES.get("sigils_file")
102
+ if upload:
103
+ source_text = upload.read().decode("utf-8", errors="ignore")
104
+ show_sigils_input = False
105
+ else:
106
+ single = request.POST.get("sigil", "")
107
+ if single:
108
+ source_text = (
109
+ f"[{single}]" if not single.startswith("[") else single
110
+ )
111
+ sigils_text = source_text
112
+ if source_text:
113
+ resolved_text = resolve_sigils_in_text(source_text)
114
+ show_result = True
115
+ if upload:
116
+ sigils_text = ""
117
+
118
+ context = admin.site.each_context(request)
119
+ context.update(
120
+ {
121
+ "title": _("Sigil Builder"),
122
+ "sigil_roots": roots,
123
+ "builtin_roots": builtin_roots,
124
+ "auto_fields": auto_fields,
125
+ "sigils_text": sigils_text,
126
+ "resolved_text": resolved_text,
127
+ "show_sigils_input": show_sigils_input,
128
+ "show_result": show_result,
129
+ }
130
+ )
131
+ return TemplateResponse(request, "admin/sigil_builder.html", context)
132
+
133
+
134
+ def patch_admin_sigil_builder_view() -> None:
135
+ """Add custom admin view for listing SigilRoots."""
136
+ original_get_urls = admin.site.get_urls
137
+
138
+ def get_urls():
139
+ urls = original_get_urls()
140
+ custom = [
141
+ path(
142
+ "sigil-builder/",
143
+ admin.site.admin_view(_sigil_builder_view),
144
+ name="sigil_builder",
145
+ ),
146
+ ]
147
+ return custom + urls
148
+
149
+ admin.site.get_urls = get_urls
core/sigil_context.py CHANGED
@@ -1,20 +1,20 @@
1
- from __future__ import annotations
2
-
3
- from threading import local
4
- from typing import Dict, Type
5
- from django.db import models
6
-
7
- _thread = local()
8
-
9
-
10
- def set_context(context: Dict[Type[models.Model], str]) -> None:
11
- _thread.context = context
12
-
13
-
14
- def get_context() -> Dict[Type[models.Model], str]:
15
- return getattr(_thread, "context", {})
16
-
17
-
18
- def clear_context() -> None:
19
- if hasattr(_thread, "context"):
20
- delattr(_thread, "context")
1
+ from __future__ import annotations
2
+
3
+ from threading import local
4
+ from typing import Dict, Type
5
+ from django.db import models
6
+
7
+ _thread = local()
8
+
9
+
10
+ def set_context(context: Dict[Type[models.Model], str]) -> None:
11
+ _thread.context = context
12
+
13
+
14
+ def get_context() -> Dict[Type[models.Model], str]:
15
+ return getattr(_thread, "context", {})
16
+
17
+
18
+ def clear_context() -> None:
19
+ if hasattr(_thread, "context"):
20
+ delattr(_thread, "context")