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.
- arthexis-0.1.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {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": "
|
|
47
|
-
"url": reverse("admin:
|
|
48
|
-
"label": _("
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
for
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
admin.
|
|
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")
|