arthexis 0.1.8__py3-none-any.whl → 0.1.9__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 (81) hide show
  1. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/METADATA +42 -4
  2. arthexis-0.1.9.dist-info/RECORD +92 -0
  3. arthexis-0.1.9.dist-info/licenses/LICENSE +674 -0
  4. config/__init__.py +0 -1
  5. config/auth_app.py +0 -1
  6. config/celery.py +1 -2
  7. config/context_processors.py +1 -1
  8. config/offline.py +2 -0
  9. config/settings.py +133 -16
  10. config/urls.py +65 -6
  11. core/admin.py +1226 -191
  12. core/admin_history.py +50 -0
  13. core/admindocs.py +108 -1
  14. core/apps.py +158 -3
  15. core/backends.py +46 -4
  16. core/entity.py +62 -48
  17. core/fields.py +6 -1
  18. core/github_helper.py +25 -0
  19. core/github_issues.py +172 -0
  20. core/lcd_screen.py +1 -0
  21. core/liveupdate.py +25 -0
  22. core/log_paths.py +100 -0
  23. core/mailer.py +83 -0
  24. core/middleware.py +57 -0
  25. core/models.py +1071 -264
  26. core/notifications.py +11 -1
  27. core/public_wifi.py +227 -0
  28. core/release.py +27 -20
  29. core/sigil_builder.py +131 -0
  30. core/sigil_context.py +20 -0
  31. core/sigil_resolver.py +284 -0
  32. core/system.py +129 -10
  33. core/tasks.py +118 -19
  34. core/test_system_info.py +22 -0
  35. core/tests.py +358 -63
  36. core/tests_liveupdate.py +17 -0
  37. core/urls.py +2 -2
  38. core/user_data.py +329 -167
  39. core/views.py +383 -57
  40. core/widgets.py +51 -0
  41. core/workgroup_urls.py +7 -3
  42. core/workgroup_views.py +43 -6
  43. nodes/actions.py +0 -2
  44. nodes/admin.py +159 -284
  45. nodes/apps.py +9 -15
  46. nodes/backends.py +53 -0
  47. nodes/lcd.py +24 -10
  48. nodes/models.py +375 -178
  49. nodes/tasks.py +1 -5
  50. nodes/tests.py +524 -129
  51. nodes/utils.py +13 -2
  52. nodes/views.py +66 -23
  53. ocpp/admin.py +150 -61
  54. ocpp/apps.py +1 -1
  55. ocpp/consumers.py +432 -69
  56. ocpp/evcs.py +25 -8
  57. ocpp/models.py +408 -68
  58. ocpp/simulator.py +13 -6
  59. ocpp/store.py +258 -30
  60. ocpp/tasks.py +11 -7
  61. ocpp/test_export_import.py +8 -7
  62. ocpp/test_rfid.py +211 -16
  63. ocpp/tests.py +1198 -135
  64. ocpp/transactions_io.py +68 -22
  65. ocpp/urls.py +35 -2
  66. ocpp/views.py +654 -101
  67. pages/admin.py +173 -13
  68. pages/checks.py +0 -1
  69. pages/context_processors.py +19 -6
  70. pages/middleware.py +153 -0
  71. pages/models.py +37 -9
  72. pages/tests.py +759 -40
  73. pages/urls.py +3 -0
  74. pages/utils.py +0 -1
  75. pages/views.py +576 -25
  76. arthexis-0.1.8.dist-info/RECORD +0 -80
  77. arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
  78. config/workgroup_app.py +0 -7
  79. core/checks.py +0 -29
  80. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
  81. {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
config/auth_app.py CHANGED
@@ -5,4 +5,3 @@ class AuthConfig(DjangoAuthConfig):
5
5
  """Use a shorter label for the auth section in the admin."""
6
6
 
7
7
  verbose_name = "AUTH"
8
-
config/celery.py CHANGED
@@ -9,7 +9,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
9
9
 
10
10
  # When running on production-oriented nodes, avoid Celery debug mode.
11
11
  NODE_ROLE = os.environ.get("NODE_ROLE", "")
12
- if NODE_ROLE in {"Constellation", "Satellite", "Virtual"}:
12
+ if NODE_ROLE in {"Constellation", "Satellite"}:
13
13
  for var in ["CELERY_TRACE_APP", "CELERY_DEBUG"]:
14
14
  os.environ.pop(var, None)
15
15
  os.environ.setdefault("CELERY_LOG_LEVEL", "INFO")
@@ -23,4 +23,3 @@ app.autodiscover_tasks()
23
23
  def debug_task(self): # pragma: no cover - debug helper
24
24
  """A simple debug task."""
25
25
  print(f"Request: {self.request!r}")
26
-
@@ -13,7 +13,7 @@ def site_and_node(request: HttpRequest):
13
13
  ``badge_node`` is a ``Node`` instance or ``None`` if no match.
14
14
  ``badge_site_color`` and ``badge_node_color`` provide the configured colors.
15
15
  """
16
- host = request.get_host().split(':')[0]
16
+ host = request.get_host().split(":")[0]
17
17
  site = Site.objects.filter(domain__iexact=host).first()
18
18
 
19
19
  node = None
config/offline.py CHANGED
@@ -2,6 +2,7 @@ import os
2
2
  import functools
3
3
  import asyncio
4
4
 
5
+
5
6
  class OfflineError(RuntimeError):
6
7
  """Raised when a network operation is attempted in offline mode."""
7
8
 
@@ -21,6 +22,7 @@ def requires_network(func):
21
22
  """
22
23
 
23
24
  if asyncio.iscoroutinefunction(func):
25
+
24
26
  @functools.wraps(func)
25
27
  async def async_wrapper(*args, **kwargs):
26
28
  if _is_offline():
config/settings.py CHANGED
@@ -16,11 +16,15 @@ import os
16
16
  import sys
17
17
  import ipaddress
18
18
  import socket
19
+ from core.log_paths import select_log_dir
19
20
  from django.utils.translation import gettext_lazy as _
20
21
  from celery.schedules import crontab
21
22
  from django.http import request as http_request
22
23
  from django.middleware.csrf import CsrfViewMiddleware
23
24
  from django.core.exceptions import DisallowedHost
25
+ from django.contrib.sites import shortcuts as sites_shortcuts
26
+ from django.contrib.sites.requests import RequestSite
27
+ from django.core.management.utils import get_random_secret_key
24
28
  from urllib.parse import urlsplit
25
29
  import django.utils.encoding as encoding
26
30
 
@@ -57,7 +61,9 @@ ACRONYMS: list[str] = []
57
61
  with contextlib.suppress(FileNotFoundError):
58
62
  ACRONYMS = [
59
63
  line.strip()
60
- for line in (BASE_DIR / "config" / "data" / "ACRONYMS.txt").read_text().splitlines()
64
+ for line in (BASE_DIR / "config" / "data" / "ACRONYMS.txt")
65
+ .read_text()
66
+ .splitlines()
61
67
  if line.strip()
62
68
  ]
63
69
 
@@ -66,7 +72,29 @@ with contextlib.suppress(FileNotFoundError):
66
72
  # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
67
73
 
68
74
  # SECURITY WARNING: keep the secret key used in production secret!
69
- SECRET_KEY = "django-insecure-16%ed9hv^gg!jj5ff6d4w=t$50k^abkq75+vwl44%^+qyq!m#w"
75
+
76
+
77
+ def _load_secret_key() -> str:
78
+ for env_var in ("DJANGO_SECRET_KEY", "SECRET_KEY"):
79
+ value = os.environ.get(env_var)
80
+ if value:
81
+ return value
82
+
83
+ secret_file = BASE_DIR / "locks" / "django-secret.key"
84
+ with contextlib.suppress(OSError):
85
+ stored_key = secret_file.read_text(encoding="utf-8").strip()
86
+ if stored_key:
87
+ return stored_key
88
+
89
+ generated_key = get_random_secret_key()
90
+ with contextlib.suppress(OSError):
91
+ secret_file.parent.mkdir(parents=True, exist_ok=True)
92
+ secret_file.write_text(generated_key, encoding="utf-8")
93
+
94
+ return generated_key
95
+
96
+
97
+ SECRET_KEY = _load_secret_key()
70
98
 
71
99
  # SECURITY WARNING: don't run with debug turned on in production!
72
100
 
@@ -89,6 +117,39 @@ ALLOWED_HOSTS = [
89
117
  ]
90
118
 
91
119
 
120
+ def _iter_local_hostnames(hostname: str, fqdn: str | None = None) -> list[str]:
121
+ """Return unique hostname variants for the current machine."""
122
+
123
+ hostnames: list[str] = []
124
+ seen: set[str] = set()
125
+
126
+ def _append(candidate: str | None) -> None:
127
+ if not candidate:
128
+ return
129
+ normalized = candidate.strip()
130
+ if not normalized or normalized in seen:
131
+ return
132
+ hostnames.append(normalized)
133
+ seen.add(normalized)
134
+
135
+ _append(hostname)
136
+ _append(fqdn)
137
+ if hostname and "." not in hostname:
138
+ _append(f"{hostname}.local")
139
+
140
+ return hostnames
141
+
142
+
143
+ _local_hostname = socket.gethostname().strip()
144
+ _local_fqdn = ""
145
+ with contextlib.suppress(Exception):
146
+ _local_fqdn = socket.getfqdn().strip()
147
+
148
+ for host in _iter_local_hostnames(_local_hostname, _local_fqdn):
149
+ if host not in ALLOWED_HOSTS:
150
+ ALLOWED_HOSTS.append(host)
151
+
152
+
92
153
  # Allow CSRF origin verification for hosts within allowed subnets.
93
154
  _original_origin_verified = CsrfViewMiddleware._origin_verified
94
155
 
@@ -134,10 +195,12 @@ LOCAL_APPS = [
134
195
  "ocpp",
135
196
  "awg",
136
197
  "pages",
137
- "app",
198
+ "man",
199
+ "teams",
138
200
  ]
139
201
 
140
202
  INSTALLED_APPS = [
203
+ "whitenoise.runserver_nostatic",
141
204
  "django.contrib.admin",
142
205
  "django.contrib.admindocs",
143
206
  "config.auth_app.AuthConfig",
@@ -149,7 +212,6 @@ INSTALLED_APPS = [
149
212
  "django_object_actions",
150
213
  "django.contrib.sites",
151
214
  "channels",
152
- "config.workgroup_app.WorkgroupConfig",
153
215
  "config.horologia_app.HorologiaConfig",
154
216
  ] + LOCAL_APPS
155
217
 
@@ -163,8 +225,25 @@ if DEBUG:
163
225
 
164
226
  SITE_ID = 1
165
227
 
228
+ _original_get_current_site = sites_shortcuts.get_current_site
229
+
230
+
231
+ def _get_current_site_with_request_fallback(request=None):
232
+ try:
233
+ return _original_get_current_site(request)
234
+ except Exception as exc:
235
+ from django.contrib.sites.models import Site
236
+
237
+ if request is not None and isinstance(exc, Site.DoesNotExist):
238
+ return RequestSite(request)
239
+ raise
240
+
241
+
242
+ sites_shortcuts.get_current_site = _get_current_site_with_request_fallback
243
+
166
244
  MIDDLEWARE = [
167
245
  "django.middleware.security.SecurityMiddleware",
246
+ "whitenoise.middleware.WhiteNoiseMiddleware",
168
247
  "django.contrib.sessions.middleware.SessionMiddleware",
169
248
  "config.middleware.ActiveAppMiddleware",
170
249
  "django.middleware.locale.LocaleMiddleware",
@@ -172,6 +251,8 @@ MIDDLEWARE = [
172
251
  "django.middleware.csrf.CsrfViewMiddleware",
173
252
  "django.contrib.auth.middleware.AuthenticationMiddleware",
174
253
  "core.middleware.AdminHistoryMiddleware",
254
+ "core.middleware.SigilContextMiddleware",
255
+ "pages.middleware.ViewHistoryMiddleware",
175
256
  "django.contrib.messages.middleware.MessageMiddleware",
176
257
  "django.middleware.clickjacking.XFrameOptionsMiddleware",
177
258
  ]
@@ -214,6 +295,31 @@ ASGI_APPLICATION = "config.asgi.application"
214
295
  CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
215
296
 
216
297
 
298
+ # MCP sigil resolver configuration
299
+ def _env_int(name: str, default: int) -> int:
300
+ try:
301
+ return int(os.environ.get(name, default))
302
+ except (TypeError, ValueError): # pragma: no cover - defensive
303
+ return default
304
+
305
+
306
+ def _split_env_list(name: str) -> list[str]:
307
+ raw = os.environ.get(name)
308
+ if not raw:
309
+ return []
310
+ return [item.strip() for item in raw.split(",") if item.strip()]
311
+
312
+
313
+ MCP_SIGIL_SERVER = {
314
+ "host": os.environ.get("MCP_SIGIL_HOST", "127.0.0.1"),
315
+ "port": _env_int("MCP_SIGIL_PORT", 8800),
316
+ "api_keys": _split_env_list("MCP_SIGIL_API_KEYS"),
317
+ "required_scopes": ["sigils:read"],
318
+ "issuer_url": os.environ.get("MCP_SIGIL_ISSUER_URL"),
319
+ "resource_server_url": os.environ.get("MCP_SIGIL_RESOURCE_URL"),
320
+ }
321
+
322
+
217
323
  # Custom user model
218
324
  AUTH_USER_MODEL = "core.User"
219
325
 
@@ -223,7 +329,6 @@ AUTHENTICATION_BACKENDS = [
223
329
  "core.backends.RFIDBackend",
224
330
  ]
225
331
 
226
-
227
332
  # Database
228
333
  # https://docs.djangoproject.com/en/5.2/ref/settings/#databases
229
334
 
@@ -240,7 +345,7 @@ def _postgres_available() -> bool:
240
345
  "password": os.environ.get("POSTGRES_PASSWORD", ""),
241
346
  "host": os.environ.get("POSTGRES_HOST", "localhost"),
242
347
  "port": os.environ.get("POSTGRES_PORT", "5432"),
243
- "connect_timeout": 1,
348
+ "connect_timeout": 10,
244
349
  }
245
350
  try:
246
351
  with contextlib.closing(psycopg.connect(**params)):
@@ -259,6 +364,9 @@ if _postgres_available():
259
364
  "HOST": os.environ.get("POSTGRES_HOST", "localhost"),
260
365
  "PORT": os.environ.get("POSTGRES_PORT", "5432"),
261
366
  "OPTIONS": {"options": "-c timezone=UTC"},
367
+ "TEST": {
368
+ "NAME": f"{os.environ.get('POSTGRES_DB', 'postgres')}_test",
369
+ },
262
370
  }
263
371
  }
264
372
  else:
@@ -266,7 +374,8 @@ else:
266
374
  "default": {
267
375
  "ENGINE": "django.db.backends.sqlite3",
268
376
  "NAME": BASE_DIR / "db.sqlite3",
269
- "OPTIONS": {"timeout": 30},
377
+ "OPTIONS": {"timeout": 60},
378
+ "TEST": {"NAME": BASE_DIR / "test_db.sqlite3"},
270
379
  }
271
380
  }
272
381
 
@@ -298,6 +407,8 @@ LANGUAGE_CODE = "en-us"
298
407
  LANGUAGES = [
299
408
  ("en", _("English")),
300
409
  ("es", _("Spanish")),
410
+ ("fr", _("French")),
411
+ ("ru", _("Russian")),
301
412
  ]
302
413
 
303
414
  LOCALE_PATHS = [BASE_DIR / "locale"]
@@ -313,10 +424,13 @@ USE_TZ = True
313
424
  # https://docs.djangoproject.com/en/5.2/howto/static-files/
314
425
 
315
426
  STATIC_URL = "/static/"
427
+ STATIC_ROOT = BASE_DIR / "static"
428
+ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
316
429
  MEDIA_URL = "/media/"
317
430
  MEDIA_ROOT = BASE_DIR / "media"
318
431
 
319
432
  # Email settings
433
+ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
320
434
  DEFAULT_FROM_EMAIL = "arthexis@gmail.com"
321
435
  SERVER_EMAIL = DEFAULT_FROM_EMAIL
322
436
 
@@ -325,15 +439,15 @@ SERVER_EMAIL = DEFAULT_FROM_EMAIL
325
439
 
326
440
  DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
327
441
 
328
- # Bluesky domain account (optional)
329
- BSKY_HANDLE = os.environ.get("BSKY_HANDLE")
330
- BSKY_APP_PASSWORD = os.environ.get("BSKY_APP_PASSWORD")
442
+ # GitHub issue reporting
443
+ GITHUB_ISSUE_REPORTING_ENABLED = True
444
+ GITHUB_ISSUE_REPORTING_COOLDOWN = 3600 # seconds
331
445
 
332
446
  # Logging configuration
333
- LOG_DIR = BASE_DIR / "logs"
334
- LOG_DIR.mkdir(exist_ok=True)
447
+ LOG_DIR = select_log_dir(BASE_DIR)
448
+ os.environ.setdefault("ARTHEXIS_LOG_DIR", str(LOG_DIR))
335
449
  OLD_LOG_DIR = LOG_DIR / "old"
336
- OLD_LOG_DIR.mkdir(exist_ok=True)
450
+ OLD_LOG_DIR.mkdir(parents=True, exist_ok=True)
337
451
  LOG_FILE_NAME = "tests.log" if "test" in sys.argv else f"{socket.gethostname()}.log"
338
452
 
339
453
  LOGGING = {
@@ -368,8 +482,11 @@ CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
368
482
 
369
483
  CELERY_BEAT_SCHEDULE = {
370
484
  "heartbeat": {
371
- "task": "app.tasks.heartbeat",
485
+ "task": "core.tasks.heartbeat",
372
486
  "schedule": crontab(minute="*/5"),
373
- }
487
+ },
488
+ "birthday_greetings": {
489
+ "task": "core.tasks.birthday_greetings",
490
+ "schedule": crontab(hour=9, minute=0),
491
+ },
374
492
  }
375
-
config/urls.py CHANGED
@@ -13,15 +13,21 @@ from django.apps import apps
13
13
  from django.conf import settings
14
14
  from django.conf.urls.static import static
15
15
  from django.contrib import admin
16
- from django.contrib.admin import autodiscover
17
16
  from django.urls import include, path
17
+ import teams.admin # noqa: F401
18
18
  from django.views.decorators.csrf import csrf_exempt
19
+ from django.views.generic import RedirectView
19
20
  from django.views.i18n import set_language
20
21
  from django.utils.translation import gettext_lazy as _
21
22
  from core import views as core_views
22
- from core.admindocs import CommandsView
23
+ from core.admindocs import (
24
+ CommandsView,
25
+ ModelGraphIndexView,
26
+ OrderedModelIndexView,
27
+ )
28
+ from man import views as man_views
29
+ from pages import views as pages_views
23
30
 
24
- autodiscover()
25
31
  admin.site.site_header = _("Constellation")
26
32
  admin.site.site_title = _("Constellation")
27
33
 
@@ -64,17 +70,69 @@ def autodiscovered_urlpatterns():
64
70
 
65
71
 
66
72
  urlpatterns = [
73
+ path(
74
+ "admin/doc/manuals/",
75
+ man_views.admin_manual_list,
76
+ name="django-admindocs-manuals",
77
+ ),
78
+ path(
79
+ "admin/doc/manuals/<slug:slug>/",
80
+ man_views.admin_manual_detail,
81
+ name="django-admindocs-manual-detail",
82
+ ),
83
+ path(
84
+ "admin/doc/manuals/<slug:slug>/pdf/",
85
+ man_views.manual_pdf,
86
+ name="django-admindocs-manual-pdf",
87
+ ),
67
88
  path(
68
89
  "admin/doc/commands/",
69
90
  CommandsView.as_view(),
70
91
  name="django-admindocs-commands",
71
92
  ),
72
- path("admin/doc/", include("django.contrib.admindocs.urls")),
93
+ path(
94
+ "admin/doc/commands/",
95
+ RedirectView.as_view(pattern_name="django-admindocs-commands"),
96
+ ),
97
+ path(
98
+ "admin/doc/model-graphs/",
99
+ ModelGraphIndexView.as_view(),
100
+ name="django-admindocs-model-graphs",
101
+ ),
102
+ path(
103
+ "admindocs/model-graphs/",
104
+ RedirectView.as_view(pattern_name="django-admindocs-model-graphs"),
105
+ ),
106
+ path(
107
+ "admindocs/models/",
108
+ OrderedModelIndexView.as_view(),
109
+ name="django-admindocs-models-index",
110
+ ),
111
+ path("admindocs/", include("django.contrib.admindocs.urls")),
112
+ path(
113
+ "admin/doc/",
114
+ RedirectView.as_view(pattern_name="django-admindocs-docroot"),
115
+ ),
116
+ path(
117
+ "admin/model-graph/<str:app_label>/",
118
+ admin.site.admin_view(pages_views.admin_model_graph),
119
+ name="admin-model-graph",
120
+ ),
73
121
  path(
74
122
  "admin/core/releases/<int:pk>/<str:action>/",
75
123
  core_views.release_progress,
76
124
  name="release-progress",
77
125
  ),
126
+ path(
127
+ "admin/core/todos/<int:pk>/done/",
128
+ core_views.todo_done,
129
+ name="todo-done",
130
+ ),
131
+ path(
132
+ "admin/core/odoo-products/",
133
+ core_views.odoo_products,
134
+ name="odoo-products",
135
+ ),
78
136
  path("admin/", admin.site.urls),
79
137
  path("i18n/setlang/", csrf_exempt(set_language), name="set_language"),
80
138
  path("api/", include("core.workgroup_urls")),
@@ -92,9 +150,10 @@ if settings.DEBUG:
92
150
  urlpatterns = [
93
151
  path(
94
152
  "__debug__/",
95
- include(("debug_toolbar.urls", "debug_toolbar"), namespace="debug_toolbar"),
153
+ include(
154
+ ("debug_toolbar.urls", "debug_toolbar"), namespace="debug_toolbar"
155
+ ),
96
156
  )
97
157
  ] + urlpatterns
98
158
 
99
159
  urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
100
-