django-nativemojo 0.1.10__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.
- django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- testit/runner.py +262 -0
@@ -0,0 +1,174 @@
|
|
1
|
+
from importlib import import_module
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
3
|
+
from .manager import TaskManager
|
4
|
+
from mojo.tasks import manager
|
5
|
+
import os
|
6
|
+
from mojo.helpers import logit
|
7
|
+
from mojo.helpers import daemon
|
8
|
+
from mojo.helpers import paths
|
9
|
+
import time
|
10
|
+
|
11
|
+
|
12
|
+
class TaskEngine(daemon.Daemon):
|
13
|
+
"""
|
14
|
+
The TaskEngine is responsible for managing and executing tasks across different channels.
|
15
|
+
It leverages a thread pool to execute tasks concurrently and uses a task manager to maintain task states.
|
16
|
+
"""
|
17
|
+
def __init__(self, channels=["broadcast"], max_workers=5):
|
18
|
+
"""
|
19
|
+
Initialize the TaskEngine.
|
20
|
+
|
21
|
+
Args:
|
22
|
+
channels (list): A list of channel names where tasks are queued.
|
23
|
+
max_workers (int, optional): The maximum number of threads available for task execution. Defaults to 5.
|
24
|
+
"""
|
25
|
+
super().__init__("taskit", os.path.join(paths.VAR_ROOT, "taskit"))
|
26
|
+
self.manager = manager.TaskManager(channels)
|
27
|
+
self.channels = channels
|
28
|
+
if "broadcast" not in self.channels:
|
29
|
+
self.channels.append("broadcast")
|
30
|
+
self.max_workers = max_workers
|
31
|
+
self.executor = None
|
32
|
+
self.logger = logit.get_logger("taskit", "taskit.log")
|
33
|
+
|
34
|
+
def reset_running_tasks(self):
|
35
|
+
"""
|
36
|
+
Reset tasks that are stuck in a running state by moving them back to the pending state.
|
37
|
+
"""
|
38
|
+
for channel in self.channels:
|
39
|
+
for task_id in self.manager.get_running_ids(channel):
|
40
|
+
self.logger.info(f"moving task {task_id} from running to pending")
|
41
|
+
self.manager.remove_from_running(channel, task_id)
|
42
|
+
self.manager.add_to_pending(channel, task_id)
|
43
|
+
|
44
|
+
def queue_pending_tasks(self):
|
45
|
+
"""
|
46
|
+
Queue all the pending tasks for execution.
|
47
|
+
"""
|
48
|
+
for channel in self.channels:
|
49
|
+
for task_id in self.manager.get_pending_ids(channel):
|
50
|
+
self.queue_task(task_id)
|
51
|
+
|
52
|
+
def handle_message(self, message):
|
53
|
+
"""
|
54
|
+
Handle incoming messages from the channels, decoding task identifiers and queuing them for execution.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
message (dict): A dictionary with message data containing task information.
|
58
|
+
"""
|
59
|
+
self.queue_task(message['data'].decode())
|
60
|
+
|
61
|
+
def on_run_task(self, task_id):
|
62
|
+
"""
|
63
|
+
Execute a task based on its identifier by locating the relevant function and executing it.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
task_id (str): The identifier of the task to be executed.
|
67
|
+
"""
|
68
|
+
# this is a keep it thread safe with the redis connection
|
69
|
+
tman = TaskManager([])
|
70
|
+
task_data = tman.get_task(task_id)
|
71
|
+
if not task_data:
|
72
|
+
# this task has expired or no longer exists
|
73
|
+
self.logger.info(f"Task {task_id} has expired or no longer exists")
|
74
|
+
tman.remove_from_pending(task_id)
|
75
|
+
return
|
76
|
+
self.logger.info(f"Executing task {task_id}")
|
77
|
+
function_path = task_data.get('function')
|
78
|
+
module_name, func_name = function_path.rsplit('.', 1)
|
79
|
+
module = import_module(module_name)
|
80
|
+
func = getattr(module, func_name)
|
81
|
+
self.manager.remove_from_pending(task_id, task_data.channel)
|
82
|
+
self.manager.add_to_running(task_id, task_data.channel)
|
83
|
+
|
84
|
+
try:
|
85
|
+
task_data.started_at = time.time()
|
86
|
+
func(task_data)
|
87
|
+
task_data.completed_at = time.time()
|
88
|
+
task_data.elapsed_time = task_data.completed_at - task_data.started_at
|
89
|
+
tman.save_task(task_data)
|
90
|
+
tman.add_to_completed(task_data)
|
91
|
+
self.logger.info(f"Task {task_id} completed after {task_data.elapsed_time} seconds")
|
92
|
+
except Exception as e:
|
93
|
+
self.logger.error(f"Error executing task {task_id}: {str(e)}")
|
94
|
+
tman.add_to_errors(task_data, str(e))
|
95
|
+
finally:
|
96
|
+
tman.remove_from_running(task_id, task_data.channel)
|
97
|
+
|
98
|
+
def queue_task(self, task_id):
|
99
|
+
"""
|
100
|
+
Submit a task for execution in the thread pool.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
task_id (str): The identifier of the task to be queued.
|
104
|
+
"""
|
105
|
+
self.logger.info(f"adding task {task_id}")
|
106
|
+
self.executor.submit(self.on_run_task, task_id)
|
107
|
+
|
108
|
+
def wait_for_all_tasks_to_complete(self, timeout=30):
|
109
|
+
"""
|
110
|
+
Wait for all tasks submitted to the executor to complete.
|
111
|
+
"""
|
112
|
+
self.executor.shutdown(wait=True, timeout=timeout)
|
113
|
+
# Check if there are still active threads
|
114
|
+
active_threads = [thread for thread in self.executor._threads if thread.is_alive()]
|
115
|
+
if active_threads:
|
116
|
+
self.logger.warning(f"shutdown issue, {len(active_threads)} tasks exceeded timeout")
|
117
|
+
self.executor.shutdown(wait=False) # Stop accepting new tasks
|
118
|
+
|
119
|
+
def start_listening(self):
|
120
|
+
"""
|
121
|
+
Listen for messages on the subscribed channels and handle them as they arrive.
|
122
|
+
"""
|
123
|
+
self.logger.info("starting with channels...", self.channels)
|
124
|
+
self.reset_running_tasks()
|
125
|
+
self.queue_pending_tasks()
|
126
|
+
pubsub = self.manager.redis.pubsub()
|
127
|
+
channel_keys = {self.manager.get_channel_key(channel): self.handle_message for channel in self.channels}
|
128
|
+
pubsub.subscribe(**channel_keys)
|
129
|
+
for message in pubsub.listen():
|
130
|
+
if not self.running:
|
131
|
+
self.logger.info("shutting down, waiting for tasks to complete")
|
132
|
+
self.wait_for_all_tasks_to_complete()
|
133
|
+
self.logger.info("shutdown complete")
|
134
|
+
return
|
135
|
+
if message['type'] != 'message':
|
136
|
+
continue
|
137
|
+
self.handle_message(message)
|
138
|
+
|
139
|
+
def run(self):
|
140
|
+
self.executor = ThreadPoolExecutor(max_workers=self.max_workers)
|
141
|
+
self.start_listening()
|
142
|
+
|
143
|
+
|
144
|
+
# HELPERS FOR RUNNING VIA CLI
|
145
|
+
def get_args():
|
146
|
+
"""
|
147
|
+
Setup the argument parser for command-line interface.
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
Namespace: Parsed command-line arguments.
|
151
|
+
"""
|
152
|
+
import argparse
|
153
|
+
parser = argparse.ArgumentParser(description="TaskEngine Background Service")
|
154
|
+
parser.add_argument("--start", action="store_true", help="Start the daemon")
|
155
|
+
parser.add_argument("--stop", action="store_true", help="Stop the daemon")
|
156
|
+
parser.add_argument("--foreground", "-f", action="store_true", help="Run in foreground mode")
|
157
|
+
parser.add_argument("-v", "--verbose", action="store_true",
|
158
|
+
help="Enable verbose logging")
|
159
|
+
return parser, parser.parse_args()
|
160
|
+
|
161
|
+
|
162
|
+
def main():
|
163
|
+
from mojo.helpers.settings import settings
|
164
|
+
parser, args = get_args()
|
165
|
+
daemon = TaskEngine(settings.TASKIT_CHANNELS)
|
166
|
+
if args.start:
|
167
|
+
daemon.start()
|
168
|
+
elif args.stop:
|
169
|
+
daemon.stop()
|
170
|
+
elif args.foreground:
|
171
|
+
print("Running in foreground mode...")
|
172
|
+
daemon.run()
|
173
|
+
else:
|
174
|
+
parser.print_help()
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from mojo.helpers import logit
|
2
|
+
import time
|
3
|
+
|
4
|
+
logger = logit.get_logger("ti_example", "ti_example.log")
|
5
|
+
|
6
|
+
def run_example_task(task):
|
7
|
+
logger.info("Running example task with data", task)
|
8
|
+
time.sleep(task.data.get("duration", 5))
|
9
|
+
|
10
|
+
|
11
|
+
def run_error_task(task):
|
12
|
+
logger.info("Running error task with data", task)
|
13
|
+
time.sleep(2)
|
14
|
+
raise Exception("Example error")
|
mojo/decorators/auth.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
from functools import wraps
|
2
|
+
import mojo.errors
|
3
|
+
|
4
|
+
def requires_perms(*required_perms):
|
5
|
+
def decorator(func):
|
6
|
+
@wraps(func)
|
7
|
+
def wrapper(request, *args, **kwargs):
|
8
|
+
if not request.user.is_authenticated:
|
9
|
+
raise mojo.errors.PermissionDeniedException()
|
10
|
+
if not request.user.has_permission(required_perms):
|
11
|
+
raise mojo.errors.PermissionDeniedException()
|
12
|
+
return func(request, *args, **kwargs)
|
13
|
+
return wrapper
|
14
|
+
return decorator
|
15
|
+
|
16
|
+
|
17
|
+
def requires_auth():
|
18
|
+
def decorator(func):
|
19
|
+
@wraps(func)
|
20
|
+
def wrapper(request, *args, **kwargs):
|
21
|
+
if not request.user.is_authenticated:
|
22
|
+
raise mojo.errors.PermissionDeniedException()
|
23
|
+
return func(request, *args, **kwargs)
|
24
|
+
return wrapper
|
25
|
+
return decorator
|
mojo/decorators/cron.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
from typing import Callable
|
2
|
+
|
3
|
+
def schedule(minutes: str = '*', hours: str = '*', days: str = '*',
|
4
|
+
months: str = '*', weekdays: str = '*') -> Callable:
|
5
|
+
"""
|
6
|
+
A decorator to schedule functions based on cron syntax.
|
7
|
+
|
8
|
+
Args:
|
9
|
+
minutes (str): The minutes argument for the cron schedule (default is '*').
|
10
|
+
hours (str): The hours argument for the cron schedule (default is '*').
|
11
|
+
days (str): The days of the month argument for the cron schedule (default is '*').
|
12
|
+
months (str): The months argument for the cron schedule (default is '*').
|
13
|
+
weekdays (str): The days of the week argument for the cron schedule (default is '*').
|
14
|
+
|
15
|
+
Returns:
|
16
|
+
Callable: The decorated function.
|
17
|
+
"""
|
18
|
+
def decorator(func: Callable) -> Callable:
|
19
|
+
if not hasattr(decorator, 'scheduled_functions'):
|
20
|
+
decorator.scheduled_functions = []
|
21
|
+
cron_spec = {
|
22
|
+
'func': func,
|
23
|
+
'minutes': minutes,
|
24
|
+
'hours': hours,
|
25
|
+
'days': days,
|
26
|
+
'months': months,
|
27
|
+
'weekdays': weekdays
|
28
|
+
}
|
29
|
+
decorator.scheduled_functions.append(cron_spec)
|
30
|
+
return func
|
31
|
+
return decorator
|
mojo/decorators/http.py
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
import sys
|
2
|
+
import traceback
|
3
|
+
from mojo.helpers.settings import settings
|
4
|
+
from mojo.helpers import modules as jm
|
5
|
+
from mojo.helpers import logit
|
6
|
+
import mojo.errors
|
7
|
+
from django.urls import path, re_path
|
8
|
+
# from django.http import JsonResponse
|
9
|
+
from mojo.helpers.response import JsonResponse
|
10
|
+
from functools import wraps
|
11
|
+
from mojo.helpers import modules
|
12
|
+
from mojo.models import rest
|
13
|
+
from mojo.apps import metrics
|
14
|
+
|
15
|
+
logger = logit.get_logger("error", "error.log")
|
16
|
+
# logger.info("created")
|
17
|
+
|
18
|
+
# Global registry for REST routes
|
19
|
+
REGISTERED_URLS = {}
|
20
|
+
URLPATTERN_METHODS = {}
|
21
|
+
MOJO_API_MODULE = settings.get("MOJO_API_MODULE", "api")
|
22
|
+
MOJO_APPEND_SLASH = settings.get("MOJO_APPEND_SLASH", False)
|
23
|
+
|
24
|
+
API_METRICS = settings.get("API_METRICS", False)
|
25
|
+
API_METRICS_GRANULARITY = settings.get("API_METRICS_GRANULARITY", "days")
|
26
|
+
|
27
|
+
|
28
|
+
def dispatcher(request, *args, **kwargs):
|
29
|
+
"""
|
30
|
+
Dispatches incoming requests to the appropriate registered URL method.
|
31
|
+
"""
|
32
|
+
rest.ACTIVE_REQUEST = request
|
33
|
+
key = kwargs.pop('__mojo_rest_root_key__', None)
|
34
|
+
if "group" in request.DATA:
|
35
|
+
request.group = modules.get_model_instance("account", "Group", int(request.DATA.group))
|
36
|
+
method_key = f"{key}__{request.method}"
|
37
|
+
if method_key not in URLPATTERN_METHODS:
|
38
|
+
method_key = f"{key}__ALL"
|
39
|
+
if method_key in URLPATTERN_METHODS:
|
40
|
+
return dispatch_error_handler(URLPATTERN_METHODS[method_key])(request, *args, **kwargs)
|
41
|
+
return JsonResponse({"error": "Endpoint not found", "code": 404}, status=404)
|
42
|
+
|
43
|
+
|
44
|
+
def dispatch_error_handler(func):
|
45
|
+
"""
|
46
|
+
Decorator to catch and handle errors.
|
47
|
+
It logs exceptions and returns appropriate HTTP responses.
|
48
|
+
"""
|
49
|
+
@wraps(func)
|
50
|
+
def wrapper(request, *args, **kwargs):
|
51
|
+
try:
|
52
|
+
if API_METRICS:
|
53
|
+
metrics.record("api_calls", category="mojo_api", min_granularity=API_METRICS_GRANULARITY)
|
54
|
+
return func(request, *args, **kwargs)
|
55
|
+
except mojo.errors.MojoException as err:
|
56
|
+
if API_METRICS:
|
57
|
+
metrics.record("api_errors", category="mojo_api", min_granularity=API_METRICS_GRANULARITY)
|
58
|
+
return JsonResponse({"error": err.reason, "code": err.code}, status=err.status)
|
59
|
+
except ValueError as err:
|
60
|
+
if API_METRICS:
|
61
|
+
metrics.record("api_errors", category="mojo_api", min_granularity=API_METRICS_GRANULARITY)
|
62
|
+
logger.exception(f"ValueErrror: {str(err)}, Path: {request.path}, IP: {request.META.get('REMOTE_ADDR')}")
|
63
|
+
return JsonResponse({"error": str(err), "code": 555 }, status=500)
|
64
|
+
except Exception as err:
|
65
|
+
if API_METRICS:
|
66
|
+
metrics.record("api_errors", category="mojo_api", min_granularity=API_METRICS_GRANULARITY)
|
67
|
+
# logger.exception(f"Unhandled REST Exception: {request.path}")
|
68
|
+
logger.exception(f"Error: {str(err)}, Path: {request.path}, IP: {request.META.get('REMOTE_ADDR')}")
|
69
|
+
return JsonResponse({"error": str(err) }, status=500)
|
70
|
+
|
71
|
+
return wrapper
|
72
|
+
|
73
|
+
|
74
|
+
def _register_route(method="ALL"):
|
75
|
+
"""
|
76
|
+
Decorator to automatically register a Django view for a specific HTTP method.
|
77
|
+
Supports defining a custom pattern inside the decorator.
|
78
|
+
|
79
|
+
:param method: The HTTP method (GET, POST, etc.).
|
80
|
+
"""
|
81
|
+
def decorator(pattern=None, docs=None):
|
82
|
+
def wrapper(view_func):
|
83
|
+
module = jm.get_root_module(view_func)
|
84
|
+
if not module:
|
85
|
+
print("!!!!!!!")
|
86
|
+
print(sys._getframe(2).f_code.co_filename)
|
87
|
+
raise RuntimeError(f"Could not determine module for {view_func.__name__}")
|
88
|
+
|
89
|
+
# Ensure `urlpatterns` exists in the calling module
|
90
|
+
if not hasattr(module, 'urlpatterns'):
|
91
|
+
module.urlpatterns = []
|
92
|
+
|
93
|
+
# If no pattern is provided, use the function name as the pattern
|
94
|
+
if pattern is None:
|
95
|
+
pattern_used = f"{view_func.__name__}"
|
96
|
+
else:
|
97
|
+
pattern_used = pattern
|
98
|
+
|
99
|
+
if MOJO_APPEND_SLASH:
|
100
|
+
pattern_used = pattern if pattern_used.endswith("/") else f"{pattern_used}/"
|
101
|
+
# Register view in URL mapping
|
102
|
+
app_name = module.__name__.split(".")[-1]
|
103
|
+
# print(f"{module.__name__}.urlpatterns")
|
104
|
+
root_key = f"{app_name}__{pattern_used}"
|
105
|
+
key = f"{root_key}__{method}"
|
106
|
+
# print(f"{app_name} -> {pattern_used} -> {key}")
|
107
|
+
URLPATTERN_METHODS[key] = view_func
|
108
|
+
|
109
|
+
# Determine whether to use path() or re_path()
|
110
|
+
url_func = path if not (pattern_used.startswith("^") or pattern_used.endswith("$")) else re_path
|
111
|
+
|
112
|
+
# Add to `urlpatterns`
|
113
|
+
module.urlpatterns.append(url_func(
|
114
|
+
pattern_used, dispatcher,
|
115
|
+
kwargs={
|
116
|
+
"__mojo_rest_root_key__": root_key
|
117
|
+
}))
|
118
|
+
# Attach metadata
|
119
|
+
view_func.__app_module_name__ = module.__name__
|
120
|
+
view_func.__app_name__ = app_name
|
121
|
+
view_func.__url__ = (method, pattern_used)
|
122
|
+
view_func.__docs__ = docs or {}
|
123
|
+
return view_func
|
124
|
+
return wrapper
|
125
|
+
return decorator
|
126
|
+
|
127
|
+
# Public-facing URL decorators
|
128
|
+
URL = _register_route()
|
129
|
+
GET = _register_route("GET")
|
130
|
+
POST = _register_route("POST")
|
131
|
+
PUT = _register_route("PUT")
|
132
|
+
DELETE = _register_route("DELETE")
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from functools import wraps
|
2
|
+
import mojo.errors
|
3
|
+
|
4
|
+
def requires_params(*required_params):
|
5
|
+
def decorator(func):
|
6
|
+
@wraps(func)
|
7
|
+
def wrapper(request, *args, **kwargs):
|
8
|
+
missing_params = [param for param in required_params if param not in request.DATA]
|
9
|
+
if missing_params:
|
10
|
+
str_params = ', '.join(missing_params)
|
11
|
+
raise mojo.errors.ValueException(f"missing required parameters: {str_params}")
|
12
|
+
return func(request, *args, **kwargs)
|
13
|
+
return wrapper
|
14
|
+
return decorator
|
mojo/errors.py
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
class MojoException(Exception):
|
2
|
+
"""
|
3
|
+
Base exception class for Mojo-related errors.
|
4
|
+
|
5
|
+
Attributes:
|
6
|
+
reason (str): The reason for the exception.
|
7
|
+
code (int): The error code associated with the exception.
|
8
|
+
status (int, optional): The HTTP status code. Defaults to None.
|
9
|
+
"""
|
10
|
+
|
11
|
+
def __init__(self, reason, code, status=500):
|
12
|
+
"""
|
13
|
+
Initialize a MojoException instance.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
reason (str): The reason for the exception.
|
17
|
+
code (int): The error code associated with the exception.
|
18
|
+
status (int, optional): The HTTP status code. Defaults to None.
|
19
|
+
"""
|
20
|
+
super().__init__(reason)
|
21
|
+
self.reason = reason
|
22
|
+
self.code = code
|
23
|
+
self.status = status
|
24
|
+
|
25
|
+
|
26
|
+
class ValueException(MojoException):
|
27
|
+
"""
|
28
|
+
Exception raised for REST API value errors.
|
29
|
+
|
30
|
+
Attributes:
|
31
|
+
reason (str): The reason for the exception. Defaults to 'REST API Error'.
|
32
|
+
code (int): The error code associated with the exception. Defaults to 500.
|
33
|
+
status (int, optional): The HTTP status code. Defaults to 500.
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(self, reason='REST API Error', code=400, status=400):
|
37
|
+
"""
|
38
|
+
Initialize a RestErrorException instance.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
reason (str, optional): The reason for the exception. Defaults to 'REST API Error'.
|
42
|
+
code (int, optional): The error code associated with the exception. Defaults to 500.
|
43
|
+
status (int, optional): The HTTP status code. Defaults to 500.
|
44
|
+
"""
|
45
|
+
super().__init__(reason, code, status)
|
46
|
+
|
47
|
+
|
48
|
+
class PermissionDeniedException(MojoException):
|
49
|
+
"""
|
50
|
+
Exception raised for permission denied errors.
|
51
|
+
|
52
|
+
Attributes:
|
53
|
+
reason (str): The reason for the exception. Defaults to 'Permission Denied'.
|
54
|
+
code (int): The error code associated with the exception. Defaults to 403.
|
55
|
+
status (int, optional): The HTTP status code. Defaults to 403.
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(self, reason='Permission Denied', code=403, status=403):
|
59
|
+
"""
|
60
|
+
Initialize a PermissionDeniedException instance.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
reason (str, optional): The reason for the exception. Defaults to 'Permission Denied'.
|
64
|
+
code (int, optional): The error code associated with the exception. Defaults to 403.
|
65
|
+
status (int, optional): The HTTP status code. Defaults to 403.
|
66
|
+
"""
|
67
|
+
super().__init__(reason, code, status)
|
68
|
+
|
69
|
+
class RestErrorException(MojoException):
|
70
|
+
"""
|
71
|
+
Exception raised for REST API errors.
|
72
|
+
|
73
|
+
Attributes:
|
74
|
+
reason (str): The reason for the exception. Defaults to 'REST API Error'.
|
75
|
+
code (int): The error code associated with the exception. Defaults to 500.
|
76
|
+
status (int, optional): The HTTP status code. Defaults to 500.
|
77
|
+
"""
|
78
|
+
|
79
|
+
def __init__(self, reason='REST API Error', code=500, status=500):
|
80
|
+
"""
|
81
|
+
Initialize a RestErrorException instance.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
reason (str, optional): The reason for the exception. Defaults to 'REST API Error'.
|
85
|
+
code (int, optional): The error code associated with the exception. Defaults to 500.
|
86
|
+
status (int, optional): The HTTP status code. Defaults to 500.
|
87
|
+
"""
|
88
|
+
super().__init__(reason, code, status)
|
mojo/helpers/__init__.py
ADDED
File without changes
|
File without changes
|