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
config/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Config package initialization."""
|
|
2
|
-
|
|
3
|
-
from .celery import app as celery_app
|
|
4
|
-
|
|
5
|
-
__all__ = ("celery_app",)
|
|
1
|
+
"""Config package initialization."""
|
|
2
|
+
|
|
3
|
+
from .celery import app as celery_app
|
|
4
|
+
|
|
5
|
+
__all__ = ("celery_app",)
|
config/active_app.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import threading
|
|
2
|
-
import socket
|
|
3
|
-
|
|
4
|
-
_active = threading.local()
|
|
5
|
-
_active.name = socket.gethostname()
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def get_active_app():
|
|
9
|
-
"""Return the currently active app name."""
|
|
10
|
-
return getattr(_active, "name", socket.gethostname())
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def set_active_app(name: str) -> None:
|
|
14
|
-
"""Set the active app name for the current thread."""
|
|
15
|
-
_active.name = name or socket.gethostname()
|
|
1
|
+
import threading
|
|
2
|
+
import socket
|
|
3
|
+
|
|
4
|
+
_active = threading.local()
|
|
5
|
+
_active.name = socket.gethostname()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_active_app():
|
|
9
|
+
"""Return the currently active app name."""
|
|
10
|
+
return getattr(_active, "name", socket.gethostname())
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def set_active_app(name: str) -> None:
|
|
14
|
+
"""Set the active app name for the current thread."""
|
|
15
|
+
_active.name = name or socket.gethostname()
|
config/asgi.py
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ASGI config for config project.
|
|
3
|
-
|
|
4
|
-
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
5
|
-
|
|
6
|
-
For more information on this file, see
|
|
7
|
-
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import os
|
|
11
|
-
from config.loadenv import loadenv
|
|
12
|
-
from channels.auth import AuthMiddlewareStack
|
|
13
|
-
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
14
|
-
from django.core.asgi import get_asgi_application
|
|
15
|
-
import ocpp.routing
|
|
16
|
-
|
|
17
|
-
loadenv()
|
|
18
|
-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
19
|
-
|
|
20
|
-
django_asgi_app = get_asgi_application()
|
|
21
|
-
|
|
22
|
-
websocket_patterns = ocpp.routing.websocket_urlpatterns
|
|
23
|
-
|
|
24
|
-
application = ProtocolTypeRouter(
|
|
25
|
-
{
|
|
26
|
-
"http": django_asgi_app,
|
|
27
|
-
"websocket": AuthMiddlewareStack(URLRouter(websocket_patterns)),
|
|
28
|
-
}
|
|
29
|
-
)
|
|
1
|
+
"""
|
|
2
|
+
ASGI config for config project.
|
|
3
|
+
|
|
4
|
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
|
5
|
+
|
|
6
|
+
For more information on this file, see
|
|
7
|
+
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from config.loadenv import loadenv
|
|
12
|
+
from channels.auth import AuthMiddlewareStack
|
|
13
|
+
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
14
|
+
from django.core.asgi import get_asgi_application
|
|
15
|
+
import ocpp.routing
|
|
16
|
+
|
|
17
|
+
loadenv()
|
|
18
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
19
|
+
|
|
20
|
+
django_asgi_app = get_asgi_application()
|
|
21
|
+
|
|
22
|
+
websocket_patterns = ocpp.routing.websocket_urlpatterns
|
|
23
|
+
|
|
24
|
+
application = ProtocolTypeRouter(
|
|
25
|
+
{
|
|
26
|
+
"http": django_asgi_app,
|
|
27
|
+
"websocket": AuthMiddlewareStack(URLRouter(websocket_patterns)),
|
|
28
|
+
}
|
|
29
|
+
)
|
config/auth_app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from django.contrib.auth.apps import AuthConfig as DjangoAuthConfig
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class AuthConfig(DjangoAuthConfig):
|
|
5
|
-
"""Use a shorter label for the auth section in the admin."""
|
|
6
|
-
|
|
7
|
-
verbose_name = "AUTH"
|
|
1
|
+
from django.contrib.auth.apps import AuthConfig as DjangoAuthConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AuthConfig(DjangoAuthConfig):
|
|
5
|
+
"""Use a shorter label for the auth section in the admin."""
|
|
6
|
+
|
|
7
|
+
verbose_name = "AUTH"
|
config/celery.py
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
"""Celery application configuration."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
from celery import Celery
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
9
|
-
|
|
10
|
-
# When running on production-oriented nodes, avoid Celery debug mode.
|
|
11
|
-
NODE_ROLE = os.environ.get("NODE_ROLE", "")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
"""Celery application configuration."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from celery import Celery
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
9
|
+
|
|
10
|
+
# When running on production-oriented nodes, avoid Celery debug mode.
|
|
11
|
+
NODE_ROLE = os.environ.get("NODE_ROLE", "")
|
|
12
|
+
PRODUCTION_ROLES = {
|
|
13
|
+
"constellation",
|
|
14
|
+
"satellite",
|
|
15
|
+
"control",
|
|
16
|
+
"terminal",
|
|
17
|
+
"gateway",
|
|
18
|
+
}
|
|
19
|
+
if NODE_ROLE.lower() in PRODUCTION_ROLES:
|
|
20
|
+
for var in ["CELERY_TRACE_APP", "CELERY_DEBUG"]:
|
|
21
|
+
os.environ.pop(var, None)
|
|
22
|
+
os.environ.setdefault("CELERY_LOG_LEVEL", "INFO")
|
|
23
|
+
|
|
24
|
+
app = Celery("config")
|
|
25
|
+
app.config_from_object("django.conf:settings", namespace="CELERY")
|
|
26
|
+
app.autodiscover_tasks()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.task(bind=True)
|
|
30
|
+
def debug_task(self): # pragma: no cover - debug helper
|
|
31
|
+
"""A simple debug task."""
|
|
32
|
+
print(f"Request: {self.request!r}")
|
config/context_processors.py
CHANGED
|
@@ -1,68 +1,67 @@
|
|
|
1
|
-
import socket
|
|
2
|
-
|
|
3
|
-
from django.contrib.sites.models import Site
|
|
4
|
-
from django.http import HttpRequest
|
|
5
|
-
from django.conf import settings
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
node_color =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
#
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
}
|
|
1
|
+
import socket
|
|
2
|
+
|
|
3
|
+
from django.contrib.sites.models import Site
|
|
4
|
+
from django.http import HttpRequest
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEFAULT_BADGE_COLOR = "#28a745"
|
|
9
|
+
UNKNOWN_BADGE_COLOR = "#6c757d"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def site_and_node(request: HttpRequest):
|
|
13
|
+
"""Provide current Site and Node based on request host.
|
|
14
|
+
|
|
15
|
+
Returns a dict with keys ``badge_site`` and ``badge_node``.
|
|
16
|
+
``badge_site`` is a ``Site`` instance or ``None`` if no match.
|
|
17
|
+
``badge_node`` is a ``Node`` instance or ``None`` if no match.
|
|
18
|
+
``badge_site_color`` and ``badge_node_color`` report the palette color used
|
|
19
|
+
for the corresponding badge. Badges always use green when the entity is
|
|
20
|
+
known and grey when the value cannot be determined.
|
|
21
|
+
"""
|
|
22
|
+
host = request.get_host().split(":")[0]
|
|
23
|
+
site = Site.objects.filter(domain__iexact=host).first()
|
|
24
|
+
|
|
25
|
+
node = None
|
|
26
|
+
try:
|
|
27
|
+
from nodes.models import Node
|
|
28
|
+
|
|
29
|
+
node = Node.get_local()
|
|
30
|
+
if not node:
|
|
31
|
+
hostname = socket.gethostname()
|
|
32
|
+
try:
|
|
33
|
+
addresses = socket.gethostbyname_ex(hostname)[2]
|
|
34
|
+
except socket.gaierror:
|
|
35
|
+
addresses = []
|
|
36
|
+
|
|
37
|
+
node = Node.objects.filter(hostname__iexact=hostname).first()
|
|
38
|
+
if not node:
|
|
39
|
+
for addr in addresses:
|
|
40
|
+
node = Node.objects.filter(address=addr).first()
|
|
41
|
+
if node:
|
|
42
|
+
break
|
|
43
|
+
if not node:
|
|
44
|
+
node = (
|
|
45
|
+
Node.objects.filter(hostname__iexact=host).first()
|
|
46
|
+
or Node.objects.filter(address=host).first()
|
|
47
|
+
)
|
|
48
|
+
except Exception:
|
|
49
|
+
node = None
|
|
50
|
+
|
|
51
|
+
site_color = DEFAULT_BADGE_COLOR if site else UNKNOWN_BADGE_COLOR
|
|
52
|
+
node_color = DEFAULT_BADGE_COLOR if node else UNKNOWN_BADGE_COLOR
|
|
53
|
+
|
|
54
|
+
site_name = site.name if site else ""
|
|
55
|
+
node_role_name = node.role.name if node and node.role else ""
|
|
56
|
+
return {
|
|
57
|
+
"badge_site": site,
|
|
58
|
+
"badge_node": node,
|
|
59
|
+
# Public views fall back to the node role when the site name is blank.
|
|
60
|
+
"badge_site_name": site_name or node_role_name,
|
|
61
|
+
# Admin site badge uses the site display name if set, otherwise the domain.
|
|
62
|
+
"badge_admin_site_name": site_name or (site.domain if site else ""),
|
|
63
|
+
"badge_site_color": site_color,
|
|
64
|
+
"badge_node_color": node_color,
|
|
65
|
+
"current_site_domain": site.domain if site else host,
|
|
66
|
+
"TIME_ZONE": settings.TIME_ZONE,
|
|
67
|
+
}
|
config/horologia_app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from django_celery_beat.apps import BeatConfig as BaseBeatConfig
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class HorologiaConfig(BaseBeatConfig):
|
|
5
|
-
"""Customize Periodic Tasks app label."""
|
|
6
|
-
|
|
7
|
-
verbose_name = "5. Horologia"
|
|
1
|
+
from django_celery_beat.apps import BeatConfig as BaseBeatConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HorologiaConfig(BaseBeatConfig):
|
|
5
|
+
"""Customize Periodic Tasks app label."""
|
|
6
|
+
|
|
7
|
+
verbose_name = "5. Horologia"
|
config/loadenv.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from dotenv import load_dotenv
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def loadenv() -> None:
|
|
9
|
-
"""Load all .env files from the repository root."""
|
|
10
|
-
for env_file in sorted(BASE_DIR.glob("*.env")):
|
|
11
|
-
load_dotenv(env_file, override=False)
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from dotenv import load_dotenv
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def loadenv() -> None:
|
|
9
|
+
"""Load all .env files from the repository root."""
|
|
10
|
+
for env_file in sorted(BASE_DIR.glob("*.env")):
|
|
11
|
+
load_dotenv(env_file, override=False)
|
config/logging.py
CHANGED
|
@@ -1,48 +1,59 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import os
|
|
3
|
-
import glob
|
|
4
|
-
import logging
|
|
5
|
-
from logging.handlers import TimedRotatingFileHandler
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from django.conf import settings
|
|
8
|
-
|
|
9
|
-
from .active_app import get_active_app
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ActiveAppFileHandler(TimedRotatingFileHandler):
|
|
13
|
-
"""File handler that writes to a file named after the active app."""
|
|
14
|
-
|
|
15
|
-
def _current_file(self) -> Path:
|
|
16
|
-
log_dir = Path(settings.LOG_DIR)
|
|
17
|
-
log_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
-
if "test" in sys.argv:
|
|
19
|
-
return log_dir / "tests.log"
|
|
20
|
-
return log_dir / f"{get_active_app()}.log"
|
|
21
|
-
|
|
22
|
-
def emit(self, record: logging.LogRecord) -> None:
|
|
23
|
-
current = str(self._current_file())
|
|
24
|
-
if self.baseFilename != current:
|
|
25
|
-
self.baseFilename = current
|
|
26
|
-
Path(self.baseFilename).parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
-
if self.stream:
|
|
28
|
-
self.stream.close()
|
|
29
|
-
self.stream = self._open()
|
|
30
|
-
super().emit(record)
|
|
31
|
-
|
|
32
|
-
def rotation_filename(self, default_name: str) -> str:
|
|
33
|
-
"""Place rotated logs inside the old log directory."""
|
|
34
|
-
default_path = Path(default_name)
|
|
35
|
-
old_log_dir = Path(settings.OLD_LOG_DIR)
|
|
36
|
-
old_log_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
-
return str(old_log_dir / default_path.name)
|
|
38
|
-
|
|
39
|
-
def getFilesToDelete(self):
|
|
40
|
-
"""Return files to delete in the old log directory respecting backupCount."""
|
|
41
|
-
if self.backupCount <= 0:
|
|
42
|
-
return []
|
|
43
|
-
_, base_name = os.path.split(self.baseFilename)
|
|
44
|
-
files = glob.glob(os.path.join(settings.OLD_LOG_DIR, base_name + ".*"))
|
|
45
|
-
files.sort()
|
|
46
|
-
if len(files) <= self.backupCount:
|
|
47
|
-
return []
|
|
48
|
-
return files[: len(files) - self.backupCount]
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import glob
|
|
4
|
+
import logging
|
|
5
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
|
|
9
|
+
from .active_app import get_active_app
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ActiveAppFileHandler(TimedRotatingFileHandler):
|
|
13
|
+
"""File handler that writes to a file named after the active app."""
|
|
14
|
+
|
|
15
|
+
def _current_file(self) -> Path:
|
|
16
|
+
log_dir = Path(settings.LOG_DIR)
|
|
17
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
if "test" in sys.argv:
|
|
19
|
+
return log_dir / "tests.log"
|
|
20
|
+
return log_dir / f"{get_active_app()}.log"
|
|
21
|
+
|
|
22
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
23
|
+
current = str(self._current_file())
|
|
24
|
+
if self.baseFilename != current:
|
|
25
|
+
self.baseFilename = current
|
|
26
|
+
Path(self.baseFilename).parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
if self.stream:
|
|
28
|
+
self.stream.close()
|
|
29
|
+
self.stream = self._open()
|
|
30
|
+
super().emit(record)
|
|
31
|
+
|
|
32
|
+
def rotation_filename(self, default_name: str) -> str:
|
|
33
|
+
"""Place rotated logs inside the old log directory."""
|
|
34
|
+
default_path = Path(default_name)
|
|
35
|
+
old_log_dir = Path(settings.OLD_LOG_DIR)
|
|
36
|
+
old_log_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
return str(old_log_dir / default_path.name)
|
|
38
|
+
|
|
39
|
+
def getFilesToDelete(self):
|
|
40
|
+
"""Return files to delete in the old log directory respecting backupCount."""
|
|
41
|
+
if self.backupCount <= 0:
|
|
42
|
+
return []
|
|
43
|
+
_, base_name = os.path.split(self.baseFilename)
|
|
44
|
+
files = glob.glob(os.path.join(settings.OLD_LOG_DIR, base_name + ".*"))
|
|
45
|
+
files.sort()
|
|
46
|
+
if len(files) <= self.backupCount:
|
|
47
|
+
return []
|
|
48
|
+
return files[: len(files) - self.backupCount]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ErrorFileHandler(ActiveAppFileHandler):
|
|
52
|
+
"""File handler dedicated to capturing application errors."""
|
|
53
|
+
|
|
54
|
+
def _current_file(self) -> Path:
|
|
55
|
+
log_dir = Path(settings.LOG_DIR)
|
|
56
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
if "test" in sys.argv:
|
|
58
|
+
return log_dir / "tests-error.log"
|
|
59
|
+
return log_dir / "error.log"
|
config/middleware.py
CHANGED
|
@@ -1,25 +1,71 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
from .
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
import socket
|
|
2
|
+
from django.core.exceptions import DisallowedHost
|
|
3
|
+
from django.http import HttpResponsePermanentRedirect
|
|
4
|
+
from nodes.models import Node
|
|
5
|
+
from utils.sites import get_site
|
|
6
|
+
|
|
7
|
+
from .active_app import set_active_app
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ActiveAppMiddleware:
|
|
11
|
+
"""Store the current app based on the request's site."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, get_response):
|
|
14
|
+
self.get_response = get_response
|
|
15
|
+
|
|
16
|
+
def __call__(self, request):
|
|
17
|
+
site = get_site(request)
|
|
18
|
+
node = Node.get_local()
|
|
19
|
+
role_name = node.role.name if node and node.role else "Terminal"
|
|
20
|
+
active = site.name or role_name
|
|
21
|
+
set_active_app(active)
|
|
22
|
+
request.site = site
|
|
23
|
+
request.active_app = active
|
|
24
|
+
try:
|
|
25
|
+
response = self.get_response(request)
|
|
26
|
+
finally:
|
|
27
|
+
set_active_app(socket.gethostname())
|
|
28
|
+
return response
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _is_https_request(request) -> bool:
|
|
32
|
+
if request.is_secure():
|
|
33
|
+
return True
|
|
34
|
+
|
|
35
|
+
forwarded_proto = request.META.get("HTTP_X_FORWARDED_PROTO", "")
|
|
36
|
+
if forwarded_proto:
|
|
37
|
+
candidate = forwarded_proto.split(",")[0].strip().lower()
|
|
38
|
+
if candidate == "https":
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
forwarded_header = request.META.get("HTTP_FORWARDED", "")
|
|
42
|
+
for forwarded_part in forwarded_header.split(","):
|
|
43
|
+
for element in forwarded_part.split(";"):
|
|
44
|
+
key, _, value = element.partition("=")
|
|
45
|
+
if key.strip().lower() == "proto" and value.strip().strip('"').lower() == "https":
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SiteHttpsRedirectMiddleware:
|
|
52
|
+
"""Redirect HTTP traffic to HTTPS for sites that require it."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, get_response):
|
|
55
|
+
self.get_response = get_response
|
|
56
|
+
|
|
57
|
+
def __call__(self, request):
|
|
58
|
+
site = getattr(request, "site", None)
|
|
59
|
+
if site is None:
|
|
60
|
+
site = get_site(request)
|
|
61
|
+
request.site = site
|
|
62
|
+
|
|
63
|
+
if getattr(site, "require_https", False) and not _is_https_request(request):
|
|
64
|
+
try:
|
|
65
|
+
host = request.get_host()
|
|
66
|
+
except DisallowedHost: # pragma: no cover - defensive guard
|
|
67
|
+
host = request.META.get("HTTP_HOST", "")
|
|
68
|
+
redirect_url = f"https://{host}{request.get_full_path()}"
|
|
69
|
+
return HttpResponsePermanentRedirect(redirect_url)
|
|
70
|
+
|
|
71
|
+
return self.get_response(request)
|
config/offline.py
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import functools
|
|
3
|
-
import asyncio
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class OfflineError(RuntimeError):
|
|
7
|
-
"""Raised when a network operation is attempted in offline mode."""
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _is_offline() -> bool:
|
|
11
|
-
flag = os.environ.get("ARTHEXIS_OFFLINE", "").lower()
|
|
12
|
-
return flag not in ("", "0", "false", "no")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def requires_network(func):
|
|
16
|
-
"""Decorator that blocks execution when offline mode is enabled.
|
|
17
|
-
|
|
18
|
-
When the environment variable ``ARTHEXIS_OFFLINE`` is set to a truthy value,
|
|
19
|
-
any function decorated with ``@requires_network`` will raise
|
|
20
|
-
:class:`OfflineError` before executing. Works with both synchronous and
|
|
21
|
-
asynchronous callables.
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
if asyncio.iscoroutinefunction(func):
|
|
25
|
-
|
|
26
|
-
@functools.wraps(func)
|
|
27
|
-
async def async_wrapper(*args, **kwargs):
|
|
28
|
-
if _is_offline():
|
|
29
|
-
raise OfflineError(f"{func.__name__} requires network access")
|
|
30
|
-
return await func(*args, **kwargs)
|
|
31
|
-
|
|
32
|
-
return async_wrapper
|
|
33
|
-
|
|
34
|
-
@functools.wraps(func)
|
|
35
|
-
def sync_wrapper(*args, **kwargs):
|
|
36
|
-
if _is_offline():
|
|
37
|
-
raise OfflineError(f"{func.__name__} requires network access")
|
|
38
|
-
return func(*args, **kwargs)
|
|
39
|
-
|
|
40
|
-
return sync_wrapper
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def network_available() -> bool:
|
|
44
|
-
"""Return ``True`` if network operations are permitted."""
|
|
45
|
-
|
|
46
|
-
return not _is_offline()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
__all__ = ["OfflineError", "requires_network", "network_available"]
|
|
1
|
+
import os
|
|
2
|
+
import functools
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OfflineError(RuntimeError):
|
|
7
|
+
"""Raised when a network operation is attempted in offline mode."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_offline() -> bool:
|
|
11
|
+
flag = os.environ.get("ARTHEXIS_OFFLINE", "").lower()
|
|
12
|
+
return flag not in ("", "0", "false", "no")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def requires_network(func):
|
|
16
|
+
"""Decorator that blocks execution when offline mode is enabled.
|
|
17
|
+
|
|
18
|
+
When the environment variable ``ARTHEXIS_OFFLINE`` is set to a truthy value,
|
|
19
|
+
any function decorated with ``@requires_network`` will raise
|
|
20
|
+
:class:`OfflineError` before executing. Works with both synchronous and
|
|
21
|
+
asynchronous callables.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if asyncio.iscoroutinefunction(func):
|
|
25
|
+
|
|
26
|
+
@functools.wraps(func)
|
|
27
|
+
async def async_wrapper(*args, **kwargs):
|
|
28
|
+
if _is_offline():
|
|
29
|
+
raise OfflineError(f"{func.__name__} requires network access")
|
|
30
|
+
return await func(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
return async_wrapper
|
|
33
|
+
|
|
34
|
+
@functools.wraps(func)
|
|
35
|
+
def sync_wrapper(*args, **kwargs):
|
|
36
|
+
if _is_offline():
|
|
37
|
+
raise OfflineError(f"{func.__name__} requires network access")
|
|
38
|
+
return func(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
return sync_wrapper
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def network_available() -> bool:
|
|
44
|
+
"""Return ``True`` if network operations are permitted."""
|
|
45
|
+
|
|
46
|
+
return not _is_offline()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["OfflineError", "requires_network", "network_available"]
|