arthexis 0.1.3__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 (73) hide show
  1. arthexis-0.1.3.dist-info/METADATA +126 -0
  2. arthexis-0.1.3.dist-info/RECORD +73 -0
  3. arthexis-0.1.3.dist-info/WHEEL +5 -0
  4. arthexis-0.1.3.dist-info/licenses/LICENSE +21 -0
  5. arthexis-0.1.3.dist-info/top_level.txt +5 -0
  6. config/__init__.py +6 -0
  7. config/active_app.py +15 -0
  8. config/asgi.py +29 -0
  9. config/auth_app.py +8 -0
  10. config/celery.py +19 -0
  11. config/context_processors.py +68 -0
  12. config/loadenv.py +11 -0
  13. config/logging.py +43 -0
  14. config/middleware.py +25 -0
  15. config/offline.py +47 -0
  16. config/settings.py +374 -0
  17. config/urls.py +91 -0
  18. config/wsgi.py +17 -0
  19. core/__init__.py +0 -0
  20. core/admin.py +830 -0
  21. core/apps.py +67 -0
  22. core/backends.py +82 -0
  23. core/entity.py +97 -0
  24. core/environment.py +43 -0
  25. core/fields.py +70 -0
  26. core/lcd_screen.py +77 -0
  27. core/middleware.py +34 -0
  28. core/models.py +1277 -0
  29. core/notifications.py +95 -0
  30. core/release.py +451 -0
  31. core/system.py +111 -0
  32. core/tasks.py +100 -0
  33. core/tests.py +483 -0
  34. core/urls.py +11 -0
  35. core/user_data.py +333 -0
  36. core/views.py +431 -0
  37. nodes/__init__.py +0 -0
  38. nodes/actions.py +72 -0
  39. nodes/admin.py +347 -0
  40. nodes/apps.py +76 -0
  41. nodes/lcd.py +151 -0
  42. nodes/models.py +577 -0
  43. nodes/tasks.py +50 -0
  44. nodes/tests.py +1072 -0
  45. nodes/urls.py +13 -0
  46. nodes/utils.py +62 -0
  47. nodes/views.py +262 -0
  48. ocpp/__init__.py +0 -0
  49. ocpp/admin.py +392 -0
  50. ocpp/apps.py +24 -0
  51. ocpp/consumers.py +267 -0
  52. ocpp/evcs.py +911 -0
  53. ocpp/models.py +300 -0
  54. ocpp/routing.py +9 -0
  55. ocpp/simulator.py +357 -0
  56. ocpp/store.py +175 -0
  57. ocpp/tasks.py +27 -0
  58. ocpp/test_export_import.py +129 -0
  59. ocpp/test_rfid.py +345 -0
  60. ocpp/tests.py +1229 -0
  61. ocpp/transactions_io.py +119 -0
  62. ocpp/urls.py +17 -0
  63. ocpp/views.py +359 -0
  64. pages/__init__.py +0 -0
  65. pages/admin.py +231 -0
  66. pages/apps.py +10 -0
  67. pages/checks.py +41 -0
  68. pages/context_processors.py +72 -0
  69. pages/models.py +224 -0
  70. pages/tests.py +628 -0
  71. pages/urls.py +17 -0
  72. pages/utils.py +13 -0
  73. pages/views.py +191 -0
core/user_data.py ADDED
@@ -0,0 +1,333 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from io import BytesIO
5
+ from zipfile import ZipFile
6
+ import json
7
+
8
+ from django.conf import settings
9
+ from django.contrib import admin
10
+ from django.contrib import messages
11
+ from django.contrib.contenttypes.fields import GenericForeignKey
12
+ from django.contrib.contenttypes.models import ContentType
13
+ from django.core.exceptions import ValidationError
14
+ from django.core.management import call_command
15
+ from django.db import IntegrityError, models
16
+ from django.db.models.signals import post_delete, post_save
17
+ from django.dispatch import receiver
18
+ from django.template.response import TemplateResponse
19
+ from django.urls import path, reverse
20
+ from django.http import HttpResponse, HttpResponseRedirect
21
+ from django.utils.translation import gettext as _
22
+
23
+ from .entity import Entity
24
+
25
+
26
+ class UserDatum(models.Model):
27
+ """Link an :class:`Entity` instance to a user for persistence."""
28
+
29
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
30
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
31
+ object_id = models.PositiveIntegerField()
32
+ entity = GenericForeignKey("content_type", "object_id")
33
+ created_on = models.DateTimeField(auto_now_add=True)
34
+
35
+ class Meta:
36
+ unique_together = ("user", "content_type", "object_id")
37
+ verbose_name = "User Datum"
38
+ verbose_name_plural = "User Data"
39
+
40
+
41
+ # ---- Fixture utilities ---------------------------------------------------
42
+
43
+ def _data_dir() -> Path:
44
+ path = Path(settings.BASE_DIR) / "data"
45
+ path.mkdir(exist_ok=True)
46
+ return path
47
+
48
+
49
+ def _fixture_path(user, instance) -> Path:
50
+ ct = ContentType.objects.get_for_model(instance)
51
+ filename = f"{user.pk}_{ct.app_label}_{ct.model}_{instance.pk}.json"
52
+ return _data_dir() / filename
53
+
54
+
55
+ _seed_fixture_cache: dict[tuple[str, int], Path] | None = None
56
+
57
+
58
+ def _seed_fixture_path(instance) -> Path | None:
59
+ """Return the fixture file containing the given seed datum."""
60
+ global _seed_fixture_cache
61
+ if _seed_fixture_cache is None:
62
+ _seed_fixture_cache = {}
63
+ base = Path(settings.BASE_DIR)
64
+ for path in base.rglob("fixtures/*.json"):
65
+ try:
66
+ with path.open() as f:
67
+ data = json.load(f)
68
+ except Exception:
69
+ continue
70
+ for obj in data:
71
+ model = obj.get("model")
72
+ pk = obj.get("pk")
73
+ if model and pk is not None:
74
+ _seed_fixture_cache[(model, pk)] = path
75
+ key = (f"{instance._meta.app_label}.{instance._meta.model_name}", instance.pk)
76
+ return _seed_fixture_cache.get(key)
77
+
78
+
79
+ def dump_user_fixture(instance, user) -> None:
80
+ path = _fixture_path(user, instance)
81
+ app_label = instance._meta.app_label
82
+ model_name = instance._meta.model_name
83
+ call_command(
84
+ "dumpdata",
85
+ f"{app_label}.{model_name}",
86
+ indent=2,
87
+ pks=str(instance.pk),
88
+ output=str(path),
89
+ )
90
+
91
+
92
+ def delete_user_fixture(instance, user) -> None:
93
+ _fixture_path(user, instance).unlink(missing_ok=True)
94
+
95
+
96
+ # ---- Signals -------------------------------------------------------------
97
+
98
+ @receiver(post_save)
99
+ def _entity_saved(sender, instance, **kwargs):
100
+ if isinstance(instance, UserDatum):
101
+ return
102
+ try:
103
+ ct = ContentType.objects.get_for_model(instance)
104
+ qs = list(UserDatum.objects.filter(content_type=ct, object_id=instance.pk))
105
+ except Exception:
106
+ return
107
+ for ud in qs:
108
+ dump_user_fixture(instance, ud.user)
109
+
110
+
111
+ @receiver(post_delete)
112
+ def _entity_deleted(sender, instance, **kwargs):
113
+ if isinstance(instance, UserDatum):
114
+ return
115
+ try:
116
+ ct = ContentType.objects.get_for_model(instance)
117
+ qs = list(UserDatum.objects.filter(content_type=ct, object_id=instance.pk))
118
+ except Exception:
119
+ return
120
+ for ud in qs:
121
+ delete_user_fixture(instance, ud.user)
122
+ ud.delete()
123
+
124
+
125
+ @receiver(post_save, sender=UserDatum)
126
+ def _userdatum_saved(sender, instance, **kwargs):
127
+ dump_user_fixture(instance.entity, instance.user)
128
+
129
+
130
+ @receiver(post_delete, sender=UserDatum)
131
+ def _userdatum_deleted(sender, instance, **kwargs):
132
+ delete_user_fixture(instance.entity, instance.user)
133
+
134
+
135
+ # ---- Admin integration ---------------------------------------------------
136
+
137
+ class UserDatumAdminMixin(admin.ModelAdmin):
138
+ """Mixin adding a *User Datum* checkbox to change forms."""
139
+
140
+ def render_change_form(
141
+ self, request, context, add=False, change=False, form_url="", obj=None
142
+ ):
143
+ context["show_user_datum"] = True
144
+ context["show_save_as_copy"] = issubclass(self.model, Entity) or hasattr(
145
+ self.model, "clone"
146
+ )
147
+ if obj is not None:
148
+ ct = ContentType.objects.get_for_model(obj)
149
+ context["is_user_datum"] = UserDatum.objects.filter(
150
+ user=request.user, content_type=ct, object_id=obj.pk
151
+ ).exists()
152
+ else:
153
+ context["is_user_datum"] = False
154
+ return super().render_change_form(request, context, add, change, form_url, obj)
155
+
156
+ def save_model(self, request, obj, form, change):
157
+ copied = "_saveacopy" in request.POST
158
+ if copied:
159
+ obj = obj.clone() if hasattr(obj, "clone") else obj
160
+ obj.pk = None
161
+ form.instance = obj
162
+ try:
163
+ super().save_model(request, obj, form, False)
164
+ except IntegrityError:
165
+ self.message_user(
166
+ request,
167
+ _("Unable to save copy. Adjust unique fields and try again."),
168
+ messages.ERROR,
169
+ )
170
+ raise ValidationError("save_as_copy")
171
+ else:
172
+ super().save_model(request, obj, form, change)
173
+ if copied:
174
+ return
175
+ ct = ContentType.objects.get_for_model(obj)
176
+ if request.POST.get("_user_datum") == "on":
177
+ UserDatum.objects.get_or_create(
178
+ user=request.user, content_type=ct, object_id=obj.pk
179
+ )
180
+ dump_user_fixture(obj, request.user)
181
+ path = _fixture_path(request.user, obj)
182
+ self.message_user(request, f"User datum saved to {path}")
183
+ else:
184
+ qs = UserDatum.objects.filter(
185
+ user=request.user, content_type=ct, object_id=obj.pk
186
+ )
187
+ if qs.exists():
188
+ qs.delete()
189
+ delete_user_fixture(obj, request.user)
190
+
191
+
192
+ def patch_admin_user_datum() -> None:
193
+ """Mixin all registered admin classes."""
194
+ for model, model_admin in list(admin.site._registry.items()):
195
+ if model is UserDatum:
196
+ continue
197
+ if isinstance(model_admin, UserDatumAdminMixin):
198
+ continue
199
+ admin.site.unregister(model)
200
+ template = (
201
+ getattr(model_admin, "change_form_template", None)
202
+ or "admin/user_datum_change_form.html"
203
+ )
204
+ attrs = {"change_form_template": template}
205
+ Patched = type(
206
+ f"Patched{model_admin.__class__.__name__}",
207
+ (UserDatumAdminMixin, model_admin.__class__),
208
+ attrs,
209
+ )
210
+ admin.site.register(model, Patched)
211
+
212
+
213
+ def _seed_data_view(request):
214
+ """Display all entities marked as seed data."""
215
+ sections = []
216
+ for model, model_admin in admin.site._registry.items():
217
+ if not issubclass(model, Entity):
218
+ continue
219
+ objs = model.objects.filter(is_seed_data=True)
220
+ if not objs.exists():
221
+ continue
222
+ items = []
223
+ for obj in objs:
224
+ url = reverse(
225
+ f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
226
+ args=[obj.pk],
227
+ )
228
+ fixture_path = _seed_fixture_path(obj)
229
+ items.append(
230
+ {
231
+ "url": url,
232
+ "label": str(obj),
233
+ "fixture": fixture_path.name if fixture_path else "",
234
+ }
235
+ )
236
+ sections.append({"opts": model._meta, "items": items})
237
+ context = admin.site.each_context(request)
238
+ context.update({"title": _("Seed Data"), "sections": sections})
239
+ return TemplateResponse(request, "admin/data_list.html", context)
240
+
241
+
242
+ def _user_data_view(request):
243
+ """Display all user datum entities for the current user."""
244
+ sections = {}
245
+ qs = UserDatum.objects.filter(user=request.user).select_related("content_type")
246
+ for ud in qs:
247
+ model = ud.content_type.model_class()
248
+ obj = ud.entity
249
+ url = reverse(
250
+ f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
251
+ args=[obj.pk],
252
+ )
253
+ section = sections.setdefault(model._meta, [])
254
+ fixture = _fixture_path(request.user, obj)
255
+ section.append(
256
+ {"url": url, "label": str(obj), "fixture": fixture.name}
257
+ )
258
+ section_list = [{"opts": opts, "items": items} for opts, items in sections.items()]
259
+ context = admin.site.each_context(request)
260
+ context.update(
261
+ {
262
+ "title": _("User Data"),
263
+ "sections": section_list,
264
+ "import_export": True,
265
+ }
266
+ )
267
+ return TemplateResponse(request, "admin/data_list.html", context)
268
+
269
+
270
+ def _user_data_export(request):
271
+ """Return a zip file containing all fixtures for the current user."""
272
+ buffer = BytesIO()
273
+ with ZipFile(buffer, "w") as zf:
274
+ for path in _data_dir().glob(f"{request.user.pk}_*.json"):
275
+ zf.write(path, arcname=path.name)
276
+ buffer.seek(0)
277
+ response = HttpResponse(buffer.getvalue(), content_type="application/zip")
278
+ response["Content-Disposition"] = (
279
+ f"attachment; filename=user_data_{request.user.pk}.zip"
280
+ )
281
+ return response
282
+
283
+
284
+ def _user_data_import(request):
285
+ """Import fixtures from an uploaded zip file."""
286
+ if request.method == "POST" and request.FILES.get("data_zip"):
287
+ with ZipFile(request.FILES["data_zip"]) as zf:
288
+ paths = []
289
+ data_dir = _data_dir()
290
+ for name in zf.namelist():
291
+ if not name.endswith(".json"):
292
+ continue
293
+ target = data_dir / name
294
+ with target.open("wb") as f:
295
+ f.write(zf.read(name))
296
+ paths.append(target)
297
+ if paths:
298
+ call_command("loaddata", *[str(p) for p in paths])
299
+ for p in paths:
300
+ try:
301
+ user_id, app_label, model, obj_id = p.stem.split("_", 3)
302
+ ct = ContentType.objects.get_by_natural_key(app_label, model)
303
+ UserDatum.objects.get_or_create(
304
+ user_id=int(user_id), content_type=ct, object_id=int(obj_id)
305
+ )
306
+ except Exception:
307
+ continue
308
+ return HttpResponseRedirect(reverse("admin:user_data"))
309
+
310
+
311
+ def patch_admin_user_data_views() -> None:
312
+ """Add custom admin views for seed and user data listings."""
313
+ original_get_urls = admin.site.get_urls
314
+
315
+ def get_urls():
316
+ urls = original_get_urls()
317
+ custom = [
318
+ path("seed-data/", admin.site.admin_view(_seed_data_view), name="seed_data"),
319
+ path("user-data/", admin.site.admin_view(_user_data_view), name="user_data"),
320
+ path(
321
+ "user-data/export/",
322
+ admin.site.admin_view(_user_data_export),
323
+ name="user_data_export",
324
+ ),
325
+ path(
326
+ "user-data/import/",
327
+ admin.site.admin_view(_user_data_import),
328
+ name="user_data_import",
329
+ ),
330
+ ]
331
+ return custom + urls
332
+
333
+ admin.site.get_urls = get_urls