arthexis 0.1.9__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 (112) hide show
  1. arthexis-0.1.26.dist-info/METADATA +272 -0
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +29 -29
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -25
  9. config/context_processors.py +67 -68
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +71 -25
  14. config/offline.py +49 -49
  15. config/settings.py +676 -492
  16. config/settings_helpers.py +109 -0
  17. config/urls.py +228 -159
  18. config/wsgi.py +17 -17
  19. core/admin.py +4052 -2066
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +192 -151
  22. core/apps.py +350 -223
  23. core/auto_upgrade.py +72 -0
  24. core/backends.py +311 -124
  25. core/changelog.py +403 -0
  26. core/entity.py +149 -133
  27. core/environment.py +60 -43
  28. core/fields.py +168 -75
  29. core/form_fields.py +75 -0
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +183 -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 +89 -83
  37. core/middleware.py +91 -91
  38. core/models.py +5041 -2195
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +107 -0
  42. core/release.py +940 -346
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -131
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +250 -284
  47. core/system.py +1425 -230
  48. core/tasks.py +538 -199
  49. core/temp_passwords.py +181 -0
  50. core/test_system_info.py +202 -43
  51. core/tests.py +2673 -1069
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +681 -495
  55. core/views.py +2484 -789
  56. core/widgets.py +213 -51
  57. nodes/admin.py +2236 -445
  58. nodes/apps.py +98 -70
  59. nodes/backends.py +160 -53
  60. nodes/dns.py +203 -0
  61. nodes/feature_checks.py +133 -0
  62. nodes/lcd.py +165 -165
  63. nodes/models.py +2375 -870
  64. nodes/reports.py +411 -0
  65. nodes/rfid_sync.py +210 -0
  66. nodes/signals.py +18 -0
  67. nodes/tasks.py +141 -46
  68. nodes/tests.py +5045 -1489
  69. nodes/urls.py +29 -13
  70. nodes/utils.py +172 -73
  71. nodes/views.py +1768 -304
  72. ocpp/admin.py +1775 -481
  73. ocpp/apps.py +25 -25
  74. ocpp/consumers.py +1843 -630
  75. ocpp/evcs.py +844 -928
  76. ocpp/evcs_discovery.py +158 -0
  77. ocpp/models.py +1417 -640
  78. ocpp/network.py +398 -0
  79. ocpp/reference_utils.py +42 -0
  80. ocpp/routing.py +11 -9
  81. ocpp/simulator.py +745 -368
  82. ocpp/status_display.py +26 -0
  83. ocpp/store.py +603 -403
  84. ocpp/tasks.py +479 -31
  85. ocpp/test_export_import.py +131 -130
  86. ocpp/test_rfid.py +1072 -540
  87. ocpp/tests.py +5494 -2296
  88. ocpp/transactions_io.py +197 -165
  89. ocpp/urls.py +50 -50
  90. ocpp/views.py +2024 -912
  91. pages/admin.py +1123 -396
  92. pages/apps.py +45 -10
  93. pages/checks.py +40 -40
  94. pages/context_processors.py +151 -85
  95. pages/defaults.py +13 -0
  96. pages/forms.py +221 -0
  97. pages/middleware.py +213 -153
  98. pages/models.py +720 -252
  99. pages/module_defaults.py +156 -0
  100. pages/site_config.py +137 -0
  101. pages/tasks.py +74 -0
  102. pages/tests.py +4009 -1389
  103. pages/urls.py +38 -20
  104. pages/utils.py +93 -12
  105. pages/views.py +1736 -762
  106. arthexis-0.1.9.dist-info/METADATA +0 -168
  107. arthexis-0.1.9.dist-info/RECORD +0 -92
  108. core/workgroup_urls.py +0 -17
  109. core/workgroup_views.py +0 -94
  110. nodes/actions.py +0 -70
  111. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  112. {arthexis-0.1.9.dist-info → arthexis-0.1.26.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,131 +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", "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": _("Environment"),
44
- },
45
- {
46
- "prefix": "SYS",
47
- "url": reverse("admin:system"),
48
- "label": _("System"),
49
- },
50
- ]
51
- for root in SigilRoot.objects.filter(
52
- context_type=SigilRoot.Context.ENTITY
53
- ).select_related("content_type"):
54
- if not root.content_type:
55
- continue
56
- model = root.content_type.model_class()
57
- model_name = model._meta.object_name
58
- entry = grouped.setdefault(
59
- model_name,
60
- {
61
- "model": model_name,
62
- "fields": [f.name.upper() for f in model._meta.fields],
63
- "prefixes": [],
64
- },
65
- )
66
- entry["prefixes"].append(root.prefix.upper())
67
- roots = sorted(grouped.values(), key=lambda r: r["model"])
68
- for entry in roots:
69
- entry["prefixes"].sort()
70
-
71
- auto_fields = []
72
- seen = set()
73
- for model in apps.get_models():
74
- model_name = model._meta.object_name
75
- if model_name in seen:
76
- continue
77
- seen.add(model_name)
78
- prefixes = grouped.get(model_name, {}).get("prefixes", [])
79
- for field in model._meta.fields:
80
- if isinstance(field, SigilAutoFieldMixin):
81
- auto_fields.append(
82
- {
83
- "model": model_name,
84
- "roots": prefixes,
85
- "field": field.name.upper(),
86
- }
87
- )
88
-
89
- sigils_text = ""
90
- resolved_text = ""
91
- if request.method == "POST":
92
- sigils_text = request.POST.get("sigils_text", "")
93
- upload = request.FILES.get("sigils_file")
94
- if upload:
95
- sigils_text = upload.read().decode("utf-8", errors="ignore")
96
- else:
97
- single = request.POST.get("sigil", "")
98
- if single:
99
- sigils_text = f"[{single}]" if not single.startswith("[") else single
100
- resolved_text = resolve_sigils_in_text(sigils_text) if sigils_text else ""
101
-
102
- context = admin.site.each_context(request)
103
- context.update(
104
- {
105
- "title": _("Sigil Builder"),
106
- "sigil_roots": roots,
107
- "builtin_roots": builtin_roots,
108
- "auto_fields": auto_fields,
109
- "sigils_text": sigils_text,
110
- "resolved_text": resolved_text,
111
- }
112
- )
113
- return TemplateResponse(request, "admin/sigil_builder.html", context)
114
-
115
-
116
- def patch_admin_sigil_builder_view() -> None:
117
- """Add custom admin view for listing SigilRoots."""
118
- original_get_urls = admin.site.get_urls
119
-
120
- def get_urls():
121
- urls = original_get_urls()
122
- custom = [
123
- path(
124
- "sigil-builder/",
125
- admin.site.admin_view(_sigil_builder_view),
126
- name="sigil_builder",
127
- ),
128
- ]
129
- return custom + urls
130
-
131
- 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": _("Environment"),
44
+ },
45
+ {
46
+ "prefix": "CONF",
47
+ "url": reverse("admin:config"),
48
+ "label": _("Django 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")