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/apps.py ADDED
@@ -0,0 +1,67 @@
1
+ from django.apps import AppConfig
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class CoreConfig(AppConfig):
6
+ default_auto_field = "django.db.models.BigAutoField"
7
+ name = "core"
8
+ verbose_name = _("Business Models")
9
+
10
+ def ready(self): # pragma: no cover - called by Django
11
+ from django.contrib.auth import get_user_model
12
+ from django.db.models.signals import post_migrate
13
+ from .user_data import (
14
+ patch_admin_user_datum,
15
+ patch_admin_user_data_views,
16
+ )
17
+ from .system import patch_admin_system_view
18
+ from .environment import patch_admin_environment_view
19
+
20
+ def create_default_arthexis(**kwargs):
21
+ User = get_user_model()
22
+ if not User.all_objects.exists():
23
+ User.all_objects.create_superuser(
24
+ pk=1,
25
+ username="arthexis",
26
+ email="arthexis@gmail.com",
27
+ password="arthexis",
28
+ )
29
+
30
+ post_migrate.connect(create_default_arthexis, sender=self)
31
+ patch_admin_user_datum()
32
+ patch_admin_user_data_views()
33
+ patch_admin_system_view()
34
+ patch_admin_environment_view()
35
+
36
+ from pathlib import Path
37
+ from django.conf import settings
38
+
39
+ lock = Path(settings.BASE_DIR) / "locks" / "celery.lck"
40
+
41
+ if lock.exists():
42
+
43
+ def ensure_email_collector_task(**kwargs):
44
+ try: # pragma: no cover - optional dependency
45
+ from django_celery_beat.models import (
46
+ IntervalSchedule,
47
+ PeriodicTask,
48
+ )
49
+ from django.db.utils import OperationalError, ProgrammingError
50
+ except Exception: # pragma: no cover - tables or module not ready
51
+ return
52
+
53
+ try:
54
+ schedule, _ = IntervalSchedule.objects.get_or_create(
55
+ every=1, period=IntervalSchedule.HOURS
56
+ )
57
+ PeriodicTask.objects.get_or_create(
58
+ name="poll_email_collectors",
59
+ defaults={
60
+ "interval": schedule,
61
+ "task": "core.tasks.poll_email_collectors",
62
+ },
63
+ )
64
+ except (OperationalError, ProgrammingError):
65
+ pass
66
+
67
+ post_migrate.connect(ensure_email_collector_task, sender=self)
core/backends.py ADDED
@@ -0,0 +1,82 @@
1
+ """Custom authentication backends for the core app."""
2
+
3
+ from django.contrib.auth import get_user_model
4
+ from django.contrib.auth.backends import ModelBackend
5
+ import ipaddress
6
+
7
+ from .models import EnergyAccount
8
+
9
+
10
+ class RFIDBackend:
11
+ """Authenticate using a user's RFID."""
12
+
13
+ def authenticate(self, request, rfid=None, **kwargs):
14
+ if not rfid:
15
+ return None
16
+ account = (
17
+ EnergyAccount.objects.filter(
18
+ rfids__rfid=rfid.upper(), rfids__allowed=True, user__isnull=False
19
+ )
20
+ .select_related("user")
21
+ .first()
22
+ )
23
+ if account:
24
+ return account.user
25
+ return None
26
+
27
+ def get_user(self, user_id):
28
+ User = get_user_model()
29
+ try:
30
+ return User.objects.get(pk=user_id)
31
+ except User.DoesNotExist:
32
+ return None
33
+
34
+
35
+ class LocalhostAdminBackend(ModelBackend):
36
+ """Allow default admin credentials only from local networks."""
37
+
38
+ _ALLOWED_NETWORKS = [
39
+ ipaddress.ip_network("::1/128"),
40
+ ipaddress.ip_network("127.0.0.0/8"),
41
+ ipaddress.ip_network("192.168.0.0/16"),
42
+ ipaddress.ip_network("172.16.0.0/12"),
43
+ ipaddress.ip_network("10.42.0.0/16"),
44
+ ]
45
+
46
+ def authenticate(self, request, username=None, password=None, **kwargs):
47
+ if username == "admin" and password == "admin" and request is not None:
48
+ forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
49
+ if forwarded:
50
+ remote = forwarded.split(",")[0].strip()
51
+ else:
52
+ remote = request.META.get("REMOTE_ADDR", "")
53
+ try:
54
+ ip = ipaddress.ip_address(remote)
55
+ except ValueError:
56
+ return None
57
+ allowed = any(ip in net for net in self._ALLOWED_NETWORKS)
58
+ if not allowed:
59
+ return None
60
+ User = get_user_model()
61
+ user, created = User.all_objects.get_or_create(
62
+ username="admin",
63
+ defaults={
64
+ "is_staff": True,
65
+ "is_superuser": True,
66
+ },
67
+ )
68
+ if created:
69
+ user.set_password("admin")
70
+ user.save()
71
+ elif not user.check_password("admin"):
72
+ return None
73
+ return user
74
+ return super().authenticate(request, username, password, **kwargs)
75
+
76
+ def get_user(self, user_id):
77
+ User = get_user_model()
78
+ try:
79
+ return User.all_objects.get(pk=user_id)
80
+ except User.DoesNotExist:
81
+ return None
82
+
core/entity.py ADDED
@@ -0,0 +1,97 @@
1
+ import copy
2
+ import os
3
+ import re
4
+
5
+ from django.apps import apps
6
+ from django.conf import settings
7
+ from django.db import models
8
+ from django.contrib.auth.models import UserManager as DjangoUserManager
9
+
10
+
11
+ class EntityQuerySet(models.QuerySet):
12
+ def delete(self): # pragma: no cover - delegates to instance delete
13
+ deleted = 0
14
+ for obj in self:
15
+ obj.delete()
16
+ deleted += 1
17
+ return deleted, {}
18
+
19
+
20
+ class EntityManager(models.Manager):
21
+ def get_queryset(self):
22
+ return EntityQuerySet(self.model, using=self._db).filter(is_deleted=False)
23
+
24
+
25
+ class EntityUserManager(DjangoUserManager):
26
+ def get_queryset(self):
27
+ return (
28
+ EntityQuerySet(self.model, using=self._db)
29
+ .filter(is_deleted=False)
30
+ .exclude(username="admin")
31
+ )
32
+
33
+
34
+ class Entity(models.Model):
35
+ """Base model providing seed data tracking and soft deletion."""
36
+
37
+ is_seed_data = models.BooleanField(default=False, editable=False)
38
+ is_deleted = models.BooleanField(default=False, editable=False)
39
+
40
+ objects = EntityManager()
41
+ all_objects = models.Manager()
42
+
43
+ class Meta:
44
+ abstract = True
45
+
46
+ def clone(self):
47
+ """Return an unsaved copy of this instance."""
48
+ new = copy.copy(self)
49
+ new.pk = None
50
+ return new
51
+
52
+ def save(self, *args, **kwargs):
53
+ if self.pk:
54
+ try:
55
+ old = type(self).all_objects.get(pk=self.pk)
56
+ except type(self).DoesNotExist:
57
+ pass
58
+ else:
59
+ self.is_seed_data = old.is_seed_data
60
+ super().save(*args, **kwargs)
61
+
62
+ def resolve_sigils(self, field: str) -> str:
63
+ """Return ``field`` value with [ROOT.KEY] tokens resolved."""
64
+ # Find field ignoring case
65
+ name = field.lower()
66
+ fobj = next((f for f in self._meta.fields if f.name.lower() == name), None)
67
+ if not fobj:
68
+ return ""
69
+ value = self.__dict__.get(fobj.attname, "")
70
+ if value is None:
71
+ return ""
72
+ text = str(value)
73
+
74
+ pattern = re.compile(r"\[([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)\]")
75
+ SigilRoot = apps.get_model("core", "SigilRoot")
76
+
77
+ def repl(match):
78
+ root_name, key = match.group(1), match.group(2)
79
+ try:
80
+ root = SigilRoot.objects.get(prefix__iexact=root_name)
81
+ except SigilRoot.DoesNotExist:
82
+ return ""
83
+ if root.context_type == SigilRoot.Context.CONFIG:
84
+ if root.prefix.upper() == "ENV":
85
+ return os.environ.get(key, "")
86
+ if root.prefix.upper() == "SYS":
87
+ return str(getattr(settings, key, ""))
88
+ return ""
89
+
90
+ return pattern.sub(repl, text)
91
+
92
+ def delete(self, using=None, keep_parents=False):
93
+ if self.is_seed_data:
94
+ self.is_deleted = True
95
+ self.save(update_fields=["is_deleted"])
96
+ else:
97
+ super().delete(using=using, keep_parents=keep_parents)
core/environment.py ADDED
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from django.conf import settings
6
+ from django.contrib import admin
7
+ from django.template.response import TemplateResponse
8
+ from django.urls import path
9
+ from django.utils.translation import gettext_lazy as _
10
+
11
+
12
+ def _environment_view(request):
13
+ env_vars = sorted(os.environ.items())
14
+ django_settings = sorted(
15
+ [(name, getattr(settings, name)) for name in dir(settings) if name.isupper()]
16
+ )
17
+ context = admin.site.each_context(request)
18
+ context.update(
19
+ {
20
+ "title": _("Environment"),
21
+ "env_vars": env_vars,
22
+ "django_settings": django_settings,
23
+ }
24
+ )
25
+ return TemplateResponse(request, "admin/environment.html", context)
26
+
27
+
28
+ def patch_admin_environment_view() -> None:
29
+ """Add custom admin view for environment information."""
30
+ original_get_urls = admin.site.get_urls
31
+
32
+ def get_urls():
33
+ urls = original_get_urls()
34
+ custom = [
35
+ path(
36
+ "environment/",
37
+ admin.site.admin_view(_environment_view),
38
+ name="environment",
39
+ ),
40
+ ]
41
+ return custom + urls
42
+
43
+ admin.site.get_urls = get_urls
core/fields.py ADDED
@@ -0,0 +1,70 @@
1
+ from django.db import models
2
+ from django.db.models.fields import DeferredAttribute
3
+
4
+
5
+ class _BaseSigilDescriptor(DeferredAttribute):
6
+ def __set__(self, instance, value):
7
+ instance.__dict__[self.field.attname] = value
8
+
9
+
10
+ class _CheckSigilDescriptor(_BaseSigilDescriptor):
11
+ def __get__(self, instance, cls=None):
12
+ value = super().__get__(instance, cls)
13
+ if instance is None:
14
+ return value
15
+ if getattr(instance, f"{self.field.name}_resolve_sigils", False):
16
+ return instance.resolve_sigils(self.field.name)
17
+ return value
18
+
19
+
20
+ class _AutoSigilDescriptor(_BaseSigilDescriptor):
21
+ def __get__(self, instance, cls=None):
22
+ value = super().__get__(instance, cls)
23
+ if instance is None:
24
+ return value
25
+ return instance.resolve_sigils(self.field.name)
26
+
27
+
28
+ class _SigilBaseField:
29
+ def value_from_object(self, obj):
30
+ return obj.__dict__.get(self.attname)
31
+
32
+
33
+ class SigilCheckFieldMixin(_SigilBaseField):
34
+ descriptor_class = _CheckSigilDescriptor
35
+
36
+ def contribute_to_class(self, cls, name, private_only=False):
37
+ super().contribute_to_class(cls, name, private_only=private_only)
38
+ extra_name = f"{name}_resolve_sigils"
39
+ if not any(f.name == extra_name for f in cls._meta.fields):
40
+ cls.add_to_class(
41
+ extra_name,
42
+ models.BooleanField(
43
+ default=False,
44
+ verbose_name="Resolve [SIGILS] in templates",
45
+ ),
46
+ )
47
+
48
+
49
+ class SigilAutoFieldMixin(_SigilBaseField):
50
+ descriptor_class = _AutoSigilDescriptor
51
+
52
+ def contribute_to_class(self, cls, name, private_only=False):
53
+ super().contribute_to_class(cls, name, private_only=private_only)
54
+
55
+
56
+ class SigilShortCheckField(SigilCheckFieldMixin, models.CharField):
57
+ pass
58
+
59
+
60
+ class SigilLongCheckField(SigilCheckFieldMixin, models.TextField):
61
+ pass
62
+
63
+
64
+ class SigilShortAutoField(SigilAutoFieldMixin, models.CharField):
65
+ pass
66
+
67
+
68
+ class SigilLongAutoField(SigilAutoFieldMixin, models.TextField):
69
+ pass
70
+
core/lcd_screen.py ADDED
@@ -0,0 +1,77 @@
1
+ """Standalone LCD screen updater.
2
+
3
+ The script polls ``locks/lcd_screen.lck`` for up to two lines of text and
4
+ writes them to the attached LCD1602 display. If either line exceeds 16
5
+ characters the text scrolls horizontally. A third line in the lock file
6
+ can define the scroll speed in milliseconds per character (default 1000
7
+ ms).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from nodes.lcd import CharLCD1602, LCDUnavailableError
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ LOCK_FILE = Path(__file__).resolve().parents[1] / "locks" / "lcd_screen.lck"
20
+ DEFAULT_SCROLL_MS = 1000
21
+
22
+
23
+ def _read_lock_file() -> tuple[str, str, int]:
24
+ try:
25
+ lines = LOCK_FILE.read_text(encoding="utf-8").splitlines()
26
+ except FileNotFoundError:
27
+ return "", "", DEFAULT_SCROLL_MS
28
+ line1 = lines[0][:64] if len(lines) > 0 else ""
29
+ line2 = lines[1][:64] if len(lines) > 1 else ""
30
+ try:
31
+ speed = int(lines[2]) if len(lines) > 2 else DEFAULT_SCROLL_MS
32
+ except ValueError:
33
+ speed = DEFAULT_SCROLL_MS
34
+ return line1, line2, speed
35
+
36
+
37
+ def _display(lcd: CharLCD1602, line1: str, line2: str, scroll_ms: int) -> None:
38
+ scroll_sec = max(scroll_ms, 0) / 1000.0
39
+ text1 = line1[:64]
40
+ text2 = line2[:64]
41
+ pad1 = text1 + " " * 16 if len(text1) > 16 else text1.ljust(16)
42
+ pad2 = text2 + " " * 16 if len(text2) > 16 else text2.ljust(16)
43
+ steps = max(len(pad1) - 15, len(pad2) - 15)
44
+ for i in range(steps):
45
+ segment1 = pad1[i : i + 16]
46
+ segment2 = pad2[i : i + 16]
47
+ lcd.write(0, 0, segment1.ljust(16))
48
+ lcd.write(0, 1, segment2.ljust(16))
49
+ time.sleep(scroll_sec)
50
+
51
+
52
+ def main() -> None: # pragma: no cover - hardware dependent
53
+ lcd = None
54
+ last_mtime = 0.0
55
+ while True:
56
+ try:
57
+ if LOCK_FILE.exists():
58
+ mtime = LOCK_FILE.stat().st_mtime
59
+ if mtime != last_mtime or lcd is None:
60
+ line1, line2, speed = _read_lock_file()
61
+ if lcd is None:
62
+ lcd = CharLCD1602()
63
+ lcd.init_lcd()
64
+ lcd.clear()
65
+ _display(lcd, line1, line2, speed)
66
+ last_mtime = mtime
67
+ except LCDUnavailableError as exc:
68
+ logger.warning("LCD unavailable: %s", exc)
69
+ lcd = None
70
+ except Exception as exc:
71
+ logger.warning("LCD update failed: %s", exc)
72
+ lcd = None
73
+ time.sleep(0.5)
74
+
75
+
76
+ if __name__ == "__main__": # pragma: no cover - script entry point
77
+ main()
core/middleware.py ADDED
@@ -0,0 +1,34 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from .models import AdminHistory
3
+
4
+
5
+ class AdminHistoryMiddleware:
6
+ """Log recently visited admin changelists for each user."""
7
+
8
+ def __init__(self, get_response):
9
+ self.get_response = get_response
10
+
11
+ def __call__(self, request):
12
+ response = self.get_response(request)
13
+ match = getattr(request, "resolver_match", None)
14
+ if (
15
+ request.user.is_authenticated
16
+ and request.user.is_staff
17
+ and request.method == "GET"
18
+ and match
19
+ and match.url_name
20
+ and match.url_name.endswith("_changelist")
21
+ and response.status_code == 200
22
+ ):
23
+ parts = request.path.strip("/").split("/")
24
+ if len(parts) >= 3:
25
+ app_label, model_name = parts[1], parts[2]
26
+ content_type = ContentType.objects.get_by_natural_key(
27
+ app_label, model_name
28
+ )
29
+ AdminHistory.objects.update_or_create(
30
+ user=request.user,
31
+ url=request.get_full_path(),
32
+ defaults={"content_type": content_type},
33
+ )
34
+ return response