arthexis 0.1.7__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 (82) hide show
  1. arthexis-0.1.9.dist-info/METADATA +168 -0
  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 +134 -16
  10. config/urls.py +71 -3
  11. core/admin.py +1331 -165
  12. core/admin_history.py +50 -0
  13. core/admindocs.py +151 -0
  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 +1136 -259
  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 +445 -58
  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 +17 -0
  42. core/workgroup_views.py +94 -0
  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 +4 -3
  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.7.dist-info/METADATA +0 -126
  77. arthexis-0.1.7.dist-info/RECORD +0 -77
  78. arthexis-0.1.7.dist-info/licenses/LICENSE +0 -21
  79. config/workgroup_app.py +0 -7
  80. core/checks.py +0 -29
  81. {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
  82. {arthexis-0.1.7.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
 
@@ -85,9 +113,43 @@ ALLOWED_HOSTS = [
85
113
  "10.42.0.0/16",
86
114
  "192.168.0.0/16",
87
115
  "arthexis.com",
116
+ "www.arthexis.com",
88
117
  ]
89
118
 
90
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
+
91
153
  # Allow CSRF origin verification for hosts within allowed subnets.
92
154
  _original_origin_verified = CsrfViewMiddleware._origin_verified
93
155
 
@@ -133,10 +195,12 @@ LOCAL_APPS = [
133
195
  "ocpp",
134
196
  "awg",
135
197
  "pages",
136
- "app",
198
+ "man",
199
+ "teams",
137
200
  ]
138
201
 
139
202
  INSTALLED_APPS = [
203
+ "whitenoise.runserver_nostatic",
140
204
  "django.contrib.admin",
141
205
  "django.contrib.admindocs",
142
206
  "config.auth_app.AuthConfig",
@@ -148,7 +212,6 @@ INSTALLED_APPS = [
148
212
  "django_object_actions",
149
213
  "django.contrib.sites",
150
214
  "channels",
151
- "config.workgroup_app.WorkgroupConfig",
152
215
  "config.horologia_app.HorologiaConfig",
153
216
  ] + LOCAL_APPS
154
217
 
@@ -162,8 +225,25 @@ if DEBUG:
162
225
 
163
226
  SITE_ID = 1
164
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
+
165
244
  MIDDLEWARE = [
166
245
  "django.middleware.security.SecurityMiddleware",
246
+ "whitenoise.middleware.WhiteNoiseMiddleware",
167
247
  "django.contrib.sessions.middleware.SessionMiddleware",
168
248
  "config.middleware.ActiveAppMiddleware",
169
249
  "django.middleware.locale.LocaleMiddleware",
@@ -171,6 +251,8 @@ MIDDLEWARE = [
171
251
  "django.middleware.csrf.CsrfViewMiddleware",
172
252
  "django.contrib.auth.middleware.AuthenticationMiddleware",
173
253
  "core.middleware.AdminHistoryMiddleware",
254
+ "core.middleware.SigilContextMiddleware",
255
+ "pages.middleware.ViewHistoryMiddleware",
174
256
  "django.contrib.messages.middleware.MessageMiddleware",
175
257
  "django.middleware.clickjacking.XFrameOptionsMiddleware",
176
258
  ]
@@ -213,6 +295,31 @@ ASGI_APPLICATION = "config.asgi.application"
213
295
  CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
214
296
 
215
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
+
216
323
  # Custom user model
217
324
  AUTH_USER_MODEL = "core.User"
218
325
 
@@ -222,7 +329,6 @@ AUTHENTICATION_BACKENDS = [
222
329
  "core.backends.RFIDBackend",
223
330
  ]
224
331
 
225
-
226
332
  # Database
227
333
  # https://docs.djangoproject.com/en/5.2/ref/settings/#databases
228
334
 
@@ -239,7 +345,7 @@ def _postgres_available() -> bool:
239
345
  "password": os.environ.get("POSTGRES_PASSWORD", ""),
240
346
  "host": os.environ.get("POSTGRES_HOST", "localhost"),
241
347
  "port": os.environ.get("POSTGRES_PORT", "5432"),
242
- "connect_timeout": 1,
348
+ "connect_timeout": 10,
243
349
  }
244
350
  try:
245
351
  with contextlib.closing(psycopg.connect(**params)):
@@ -258,6 +364,9 @@ if _postgres_available():
258
364
  "HOST": os.environ.get("POSTGRES_HOST", "localhost"),
259
365
  "PORT": os.environ.get("POSTGRES_PORT", "5432"),
260
366
  "OPTIONS": {"options": "-c timezone=UTC"},
367
+ "TEST": {
368
+ "NAME": f"{os.environ.get('POSTGRES_DB', 'postgres')}_test",
369
+ },
261
370
  }
262
371
  }
263
372
  else:
@@ -265,7 +374,8 @@ else:
265
374
  "default": {
266
375
  "ENGINE": "django.db.backends.sqlite3",
267
376
  "NAME": BASE_DIR / "db.sqlite3",
268
- "OPTIONS": {"timeout": 30},
377
+ "OPTIONS": {"timeout": 60},
378
+ "TEST": {"NAME": BASE_DIR / "test_db.sqlite3"},
269
379
  }
270
380
  }
271
381
 
@@ -297,6 +407,8 @@ LANGUAGE_CODE = "en-us"
297
407
  LANGUAGES = [
298
408
  ("en", _("English")),
299
409
  ("es", _("Spanish")),
410
+ ("fr", _("French")),
411
+ ("ru", _("Russian")),
300
412
  ]
301
413
 
302
414
  LOCALE_PATHS = [BASE_DIR / "locale"]
@@ -312,10 +424,13 @@ USE_TZ = True
312
424
  # https://docs.djangoproject.com/en/5.2/howto/static-files/
313
425
 
314
426
  STATIC_URL = "/static/"
427
+ STATIC_ROOT = BASE_DIR / "static"
428
+ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
315
429
  MEDIA_URL = "/media/"
316
430
  MEDIA_ROOT = BASE_DIR / "media"
317
431
 
318
432
  # Email settings
433
+ EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
319
434
  DEFAULT_FROM_EMAIL = "arthexis@gmail.com"
320
435
  SERVER_EMAIL = DEFAULT_FROM_EMAIL
321
436
 
@@ -324,15 +439,15 @@ SERVER_EMAIL = DEFAULT_FROM_EMAIL
324
439
 
325
440
  DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
326
441
 
327
- # Bluesky domain account (optional)
328
- BSKY_HANDLE = os.environ.get("BSKY_HANDLE")
329
- 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
330
445
 
331
446
  # Logging configuration
332
- LOG_DIR = BASE_DIR / "logs"
333
- LOG_DIR.mkdir(exist_ok=True)
447
+ LOG_DIR = select_log_dir(BASE_DIR)
448
+ os.environ.setdefault("ARTHEXIS_LOG_DIR", str(LOG_DIR))
334
449
  OLD_LOG_DIR = LOG_DIR / "old"
335
- OLD_LOG_DIR.mkdir(exist_ok=True)
450
+ OLD_LOG_DIR.mkdir(parents=True, exist_ok=True)
336
451
  LOG_FILE_NAME = "tests.log" if "test" in sys.argv else f"{socket.gethostname()}.log"
337
452
 
338
453
  LOGGING = {
@@ -367,8 +482,11 @@ CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
367
482
 
368
483
  CELERY_BEAT_SCHEDULE = {
369
484
  "heartbeat": {
370
- "task": "app.tasks.heartbeat",
485
+ "task": "core.tasks.heartbeat",
371
486
  "schedule": crontab(minute="*/5"),
372
- }
487
+ },
488
+ "birthday_greetings": {
489
+ "task": "core.tasks.birthday_greetings",
490
+ "schedule": crontab(hour=9, minute=0),
491
+ },
373
492
  }
374
-
config/urls.py CHANGED
@@ -14,10 +14,19 @@ from django.conf import settings
14
14
  from django.conf.urls.static import static
15
15
  from django.contrib import admin
16
16
  from django.urls import include, path
17
+ import teams.admin # noqa: F401
17
18
  from django.views.decorators.csrf import csrf_exempt
19
+ from django.views.generic import RedirectView
18
20
  from django.views.i18n import set_language
19
21
  from django.utils.translation import gettext_lazy as _
20
22
  from core import views as core_views
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
21
30
 
22
31
  admin.site.site_header = _("Constellation")
23
32
  admin.site.site_title = _("Constellation")
@@ -61,14 +70,72 @@ def autodiscovered_urlpatterns():
61
70
 
62
71
 
63
72
  urlpatterns = [
64
- path("admin/doc/", include("django.contrib.admindocs.urls")),
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
+ ),
88
+ path(
89
+ "admin/doc/commands/",
90
+ CommandsView.as_view(),
91
+ name="django-admindocs-commands",
92
+ ),
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
+ ),
65
121
  path(
66
122
  "admin/core/releases/<int:pk>/<str:action>/",
67
123
  core_views.release_progress,
68
124
  name="release-progress",
69
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
+ ),
70
136
  path("admin/", admin.site.urls),
71
137
  path("i18n/setlang/", csrf_exempt(set_language), name="set_language"),
138
+ path("api/", include("core.workgroup_urls")),
72
139
  path("", include("pages.urls")),
73
140
  ]
74
141
 
@@ -83,9 +150,10 @@ if settings.DEBUG:
83
150
  urlpatterns = [
84
151
  path(
85
152
  "__debug__/",
86
- include(("debug_toolbar.urls", "debug_toolbar"), namespace="debug_toolbar"),
153
+ include(
154
+ ("debug_toolbar.urls", "debug_toolbar"), namespace="debug_toolbar"
155
+ ),
87
156
  )
88
157
  ] + urlpatterns
89
158
 
90
159
  urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
91
-