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,165 @@
|
|
1
|
+
import ujson
|
2
|
+
from django.db.models import ForeignKey, OneToOneField
|
3
|
+
from django.db.models.query import QuerySet
|
4
|
+
from django.http import HttpResponse
|
5
|
+
import datetime
|
6
|
+
from mojo.helpers import logit
|
7
|
+
|
8
|
+
logger = logit.get_logger("serializer", "serializer.log")
|
9
|
+
|
10
|
+
class GraphSerializer:
|
11
|
+
"""
|
12
|
+
Custom serializer for Django models and QuerySets that applies `RestMeta.GRAPHS` dynamically.
|
13
|
+
Supports nested relationships and different serialization graphs.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self, instance, graph="default", many=False):
|
17
|
+
"""
|
18
|
+
:param instance: Model instance or QuerySet.
|
19
|
+
:param graph: The graph type to use (e.g., "default", "list").
|
20
|
+
:param many: Boolean, if `True`, serializes a QuerySet.
|
21
|
+
"""
|
22
|
+
self.graph = graph
|
23
|
+
self.qset = None
|
24
|
+
# If it's a QuerySet, mark `many=True`
|
25
|
+
if isinstance(instance, QuerySet):
|
26
|
+
self.many = True
|
27
|
+
self.qset = instance
|
28
|
+
self.instance = list(instance) # Convert QuerySet to list for iteration
|
29
|
+
else:
|
30
|
+
self.many = many
|
31
|
+
self.instance = instance
|
32
|
+
|
33
|
+
def serialize(self):
|
34
|
+
"""
|
35
|
+
Serializes a single model instance or a QuerySet.
|
36
|
+
"""
|
37
|
+
if self.many:
|
38
|
+
return [self._serialize_instance(obj) for obj in self.instance]
|
39
|
+
return self._serialize_instance(self.instance)
|
40
|
+
|
41
|
+
def _serialize_instance(self, obj):
|
42
|
+
"""
|
43
|
+
Serializes a single model instance based on `RestMeta.GRAPHS`.
|
44
|
+
"""
|
45
|
+
if not hasattr(obj, "RestMeta") or not hasattr(obj.RestMeta, "GRAPHS"):
|
46
|
+
logger.warning("RestMeta not found")
|
47
|
+
return self._model_to_dict_custom(obj, fields=[field.name for field in obj._meta.fields])
|
48
|
+
|
49
|
+
graph_config = obj.RestMeta.GRAPHS.get(self.graph)
|
50
|
+
if graph_config is None and self.graph != "default":
|
51
|
+
self.graph = "default"
|
52
|
+
graph_config = obj.RestMeta.GRAPHS.get(self.graph)
|
53
|
+
|
54
|
+
# If graph is not defined or None, assume all fields should be included
|
55
|
+
if graph_config is None:
|
56
|
+
logger.warning(f"graph '{self.graph}' not found for {obj.__class__.__name__}")
|
57
|
+
return self._model_to_dict_custom(obj, fields=[field.name for field in obj._meta.fields])
|
58
|
+
else:
|
59
|
+
logger.info(f"{obj.__class__.__name__}:{self.graph}", graph_config)
|
60
|
+
data = self._model_to_dict_custom(obj, fields=graph_config.get("fields", None)) # Convert normal fields
|
61
|
+
|
62
|
+
# Process extra fields (methods, metadata, etc.)
|
63
|
+
extra_fields = graph_config.get("extra", [])
|
64
|
+
for field in extra_fields:
|
65
|
+
if isinstance(field, tuple): # Handle renamed method serialization
|
66
|
+
method_name, alias = field
|
67
|
+
else:
|
68
|
+
method_name, alias = field, field
|
69
|
+
if hasattr(obj, method_name):
|
70
|
+
attr = getattr(obj, method_name)
|
71
|
+
data[alias] = attr() if callable(attr) else attr
|
72
|
+
|
73
|
+
# Process related model graphs (ForeignKeys, OneToOneFields)
|
74
|
+
related_graphs = graph_config.get("graphs", {})
|
75
|
+
for related_field, sub_graph in related_graphs.items():
|
76
|
+
related_obj = getattr(obj, related_field, None)
|
77
|
+
if related_obj is not None:
|
78
|
+
# Determine if the field is a ForeignKey or OneToOneField
|
79
|
+
field_obj = obj._meta.get_field(related_field)
|
80
|
+
if isinstance(field_obj, (ForeignKey, OneToOneField)):
|
81
|
+
# Serialize related model using its corresponding graph
|
82
|
+
logger.warning(f"graph '{sub_graph}' for {related_obj.__class__.__name__}")
|
83
|
+
data[related_field] = GraphSerializer(related_obj, graph=sub_graph).serialize()
|
84
|
+
|
85
|
+
return data
|
86
|
+
|
87
|
+
def _model_to_dict_custom(self, obj, fields=None):
|
88
|
+
"""
|
89
|
+
Custom serialization method for Django model instances.
|
90
|
+
"""
|
91
|
+
data = {}
|
92
|
+
for field in obj._meta.fields:
|
93
|
+
# logger.info(field, type(field), isinstance(field, (ForeignKey, OneToOneField)))
|
94
|
+
if fields and field.name not in fields:
|
95
|
+
continue
|
96
|
+
|
97
|
+
field_value = getattr(obj, field.name)
|
98
|
+
|
99
|
+
# Handle DateTimeField serialization to epoch
|
100
|
+
if isinstance(field_value, datetime.datetime):
|
101
|
+
data[field.name] = int(field_value.timestamp())
|
102
|
+
# Handle date serialization to ISO format
|
103
|
+
elif isinstance(field_value, datetime.date):
|
104
|
+
data[field.name] = field_value.isoformat()
|
105
|
+
elif field_value is not None and isinstance(field, (ForeignKey, OneToOneField)):
|
106
|
+
data[field.name] = field_value.id
|
107
|
+
else:
|
108
|
+
data[field.name] = field_value
|
109
|
+
# logger.info(data)
|
110
|
+
return data
|
111
|
+
|
112
|
+
def to_json(self, **kwargs):
|
113
|
+
"""Returns JSON output of the serialized data."""
|
114
|
+
data = self.serialize()
|
115
|
+
if self.many:
|
116
|
+
data = dict(data=data, status=True,
|
117
|
+
size=len(data), graph=self.graph)
|
118
|
+
else:
|
119
|
+
data = dict(data=data, status=True, graph=self.graph)
|
120
|
+
data.update(dict(kwargs))
|
121
|
+
out = ujson.dumps(data)
|
122
|
+
return out
|
123
|
+
|
124
|
+
def to_response(self, request, **kwargs):
|
125
|
+
"""
|
126
|
+
Determines the response format based on the client's Accept header.
|
127
|
+
"""
|
128
|
+
# accept_header = request.headers.get('Accept', '')
|
129
|
+
# if 'text/html' in accept_header or 'text/plain' in accept_header:
|
130
|
+
# json_data = self.to_json()
|
131
|
+
# # Wrap JSON in HTML with basic formatting for color
|
132
|
+
# response_data = f"""
|
133
|
+
# <html>
|
134
|
+
# <head>
|
135
|
+
# <style>
|
136
|
+
# body {{ font-family: monospace; }}
|
137
|
+
# .string {{ color: green; }}
|
138
|
+
# .number {{ color: blue; }}
|
139
|
+
# .boolean {{ color: purple; }}
|
140
|
+
# .null {{ color: red; }}
|
141
|
+
# .key {{ color: brown; font-weight: bold; }}
|
142
|
+
# </style>
|
143
|
+
# </head>
|
144
|
+
# <body>
|
145
|
+
# <pre>{self._colorize_json(json_data)}</pre>
|
146
|
+
# </body>
|
147
|
+
# </html>
|
148
|
+
# """
|
149
|
+
# return HttpResponse(response_data, content_type='text/html')
|
150
|
+
# else:
|
151
|
+
return HttpResponse(self.to_json(**kwargs), content_type='application/json')
|
152
|
+
|
153
|
+
def _colorize_json(self, json_data):
|
154
|
+
"""Returns JSON data with HTML span wrappers for colors."""
|
155
|
+
import re
|
156
|
+
|
157
|
+
# Match string values and wrap them in span
|
158
|
+
json_data = re.sub(r'(".*?")', r'<span class="string">\1</span>', json_data)
|
159
|
+
# Match numbers and wrap them in span
|
160
|
+
json_data = re.sub(r'\b(-?\d+\.\d+|-?\d+)\b', r'<span class="number">\1</span>', json_data)
|
161
|
+
# Match boolean and null values
|
162
|
+
json_data = re.sub(r'\b(true|false|null)\b', r'<span class="\1">\1</span>', json_data)
|
163
|
+
# Match key strings
|
164
|
+
json_data = re.sub(r'(\s*".*?")\s*:', r'<span class="key">\1</span>:', json_data)
|
165
|
+
return json_data
|
@@ -0,0 +1,188 @@
|
|
1
|
+
from mojo.helpers.settings import settings
|
2
|
+
from mojo.decorators.http import URLPATTERN_METHODS
|
3
|
+
from django.apps import apps
|
4
|
+
from mojo.helpers.response import JsonResponse
|
5
|
+
|
6
|
+
|
7
|
+
API_PREFIX = "/".join([settings.get("MOJO_PREFIX", "api/").rstrip("/"), ""])
|
8
|
+
|
9
|
+
def generate_openapi_schema(title="Mojo API", version=settings.VERSION, description="Auto-generated schema"):
|
10
|
+
paths = {}
|
11
|
+
|
12
|
+
for key, view_func in URLPATTERN_METHODS.items():
|
13
|
+
try:
|
14
|
+
method, pattern = view_func.__url__
|
15
|
+
app_name = view_func.__app_name__
|
16
|
+
except AttributeError:
|
17
|
+
continue
|
18
|
+
|
19
|
+
docs = getattr(view_func, "__docs__", {})
|
20
|
+
clean_pattern = pattern.strip("^$").strip("/")
|
21
|
+
|
22
|
+
# Normalize to always use base + detail route if ALL method
|
23
|
+
if method.lower() == "all":
|
24
|
+
path_parts = clean_pattern.split("/")
|
25
|
+
model_slug = path_parts[-1]
|
26
|
+
list_path = f"{API_PREFIX}/{app_name}/{model_slug}".replace("//", "/")
|
27
|
+
detail_path = f"{list_path}/{{pk}}"
|
28
|
+
|
29
|
+
model_cls = resolve_model_from_pattern(app_name, model_slug)
|
30
|
+
if model_cls:
|
31
|
+
paths[list_path] = generate_model_rest_docs(model_cls, is_detail_route=False)
|
32
|
+
paths[detail_path] = generate_model_rest_docs(model_cls, is_detail_route=True)
|
33
|
+
continue
|
34
|
+
|
35
|
+
# Fallback: treat as-is
|
36
|
+
full_path = f"{API_PREFIX}/{app_name}/{clean_pattern}".replace("//", "/")
|
37
|
+
if not full_path.startswith("/"):
|
38
|
+
full_path = "/" + full_path
|
39
|
+
|
40
|
+
if full_path not in paths:
|
41
|
+
paths[full_path] = {}
|
42
|
+
|
43
|
+
op = method.lower()
|
44
|
+
base_parameters = docs.get("parameters", [])
|
45
|
+
if "{pk}" in full_path:
|
46
|
+
base_parameters.insert(0, {
|
47
|
+
"name": "pk",
|
48
|
+
"in": "path",
|
49
|
+
"required": True,
|
50
|
+
"schema": {"type": "integer"}
|
51
|
+
})
|
52
|
+
|
53
|
+
paths[full_path][op] = {
|
54
|
+
"summary": docs.get("summary", f"{method} {full_path}"),
|
55
|
+
"description": docs.get("description", ""),
|
56
|
+
"parameters": base_parameters,
|
57
|
+
"responses": docs.get("responses", {
|
58
|
+
"200": {"description": "Successful response"}
|
59
|
+
})
|
60
|
+
}
|
61
|
+
|
62
|
+
return {
|
63
|
+
"openapi": "3.0.0",
|
64
|
+
"info": {
|
65
|
+
"title": title,
|
66
|
+
"version": version,
|
67
|
+
"description": description,
|
68
|
+
},
|
69
|
+
"paths": paths
|
70
|
+
}
|
71
|
+
|
72
|
+
def resolve_model_from_pattern(app_name, model_slug):
|
73
|
+
try:
|
74
|
+
model_cls = apps.get_model(app_name, model_slug.capitalize())
|
75
|
+
return model_cls
|
76
|
+
except LookupError:
|
77
|
+
return None
|
78
|
+
|
79
|
+
def generate_model_rest_docs(model_cls, is_detail_route=False):
|
80
|
+
from django.db import models
|
81
|
+
base_parameters = [
|
82
|
+
{"name": "graph", "in": "query", "schema": {"type": "string", "default": "default"}},
|
83
|
+
]
|
84
|
+
|
85
|
+
list_parameters = base_parameters + [
|
86
|
+
{"name": "size", "in": "query", "schema": {"type": "integer"}},
|
87
|
+
{"name": "start", "in": "query", "schema": {"type": "integer"}},
|
88
|
+
{"name": "sort", "in": "query", "schema": {"type": "string"}},
|
89
|
+
{"name": "dr_start", "in": "query", "schema": {"type": "string", "format": "date-time"}},
|
90
|
+
{"name": "dr_end", "in": "query", "schema": {"type": "string", "format": "date-time"}},
|
91
|
+
{"name": "dr_field", "in": "query", "schema": {"type": "string"}},
|
92
|
+
]
|
93
|
+
|
94
|
+
for field in model_cls._meta.fields:
|
95
|
+
list_parameters.append({
|
96
|
+
"name": field.name,
|
97
|
+
"in": "query",
|
98
|
+
"schema": {"type": map_field_type(field)}
|
99
|
+
})
|
100
|
+
|
101
|
+
post_schema = {
|
102
|
+
"type": "object",
|
103
|
+
"properties": {
|
104
|
+
f.name: {"type": map_field_type(f)}
|
105
|
+
for f in model_cls._meta.fields if not f.auto_created
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
ops = {}
|
110
|
+
|
111
|
+
if is_detail_route:
|
112
|
+
detail_parameters = [{
|
113
|
+
"name": "pk",
|
114
|
+
"in": "path",
|
115
|
+
"required": True,
|
116
|
+
"schema": {"type": "integer"}
|
117
|
+
}] + base_parameters
|
118
|
+
|
119
|
+
ops["get"] = {
|
120
|
+
"summary": f"Retrieve a single {model_cls.__name__}",
|
121
|
+
"parameters": detail_parameters,
|
122
|
+
"responses": {
|
123
|
+
"200": {"description": "Graph-based model serialization"}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
ops["post"] = {
|
127
|
+
"summary": f"Update a {model_cls.__name__} instance",
|
128
|
+
"parameters": detail_parameters,
|
129
|
+
"requestBody": {
|
130
|
+
"required": True,
|
131
|
+
"content": {
|
132
|
+
"application/json": {"schema": post_schema}
|
133
|
+
}
|
134
|
+
},
|
135
|
+
"responses": {
|
136
|
+
"200": {"description": "Updated model instance"}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
ops["delete"] = {
|
140
|
+
"summary": f"Delete a {model_cls.__name__} instance",
|
141
|
+
"parameters": detail_parameters,
|
142
|
+
"responses": {
|
143
|
+
"204": {"description": "Deleted successfully"}
|
144
|
+
}
|
145
|
+
}
|
146
|
+
else:
|
147
|
+
ops["get"] = {
|
148
|
+
"summary": f"List {model_cls.__name__} instances",
|
149
|
+
"parameters": list_parameters,
|
150
|
+
"responses": {
|
151
|
+
"200": {"description": "List of model instances using graph serialization"}
|
152
|
+
}
|
153
|
+
}
|
154
|
+
ops["post"] = {
|
155
|
+
"summary": f"Create a new {model_cls.__name__} instance",
|
156
|
+
"parameters": base_parameters,
|
157
|
+
"requestBody": {
|
158
|
+
"required": True,
|
159
|
+
"content": {
|
160
|
+
"application/json": {"schema": post_schema}
|
161
|
+
}
|
162
|
+
},
|
163
|
+
"responses": {
|
164
|
+
"200": {"description": "Created model instance"}
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|
168
|
+
return ops
|
169
|
+
|
170
|
+
def map_field_type(field):
|
171
|
+
from django.db import models
|
172
|
+
if isinstance(field, models.IntegerField):
|
173
|
+
return "integer"
|
174
|
+
elif isinstance(field, models.BooleanField):
|
175
|
+
return "boolean"
|
176
|
+
elif isinstance(field, models.DateTimeField):
|
177
|
+
return "string"
|
178
|
+
elif isinstance(field, models.DateField):
|
179
|
+
return "string"
|
180
|
+
return "string"
|
181
|
+
|
182
|
+
|
183
|
+
def openapi_schema_view(request):
|
184
|
+
if settings.OPENAPI_DOCS_KEY is None:
|
185
|
+
return JsonResponse(dict(status=False, error="disabled"), status=404)
|
186
|
+
if settings.OPENAPI_DOCS_KEY != request.GET.get("key", ""):
|
187
|
+
return JsonResponse(dict(status=False, error="permission denied"), status=403)
|
188
|
+
return JsonResponse(generate_openapi_schema())
|
mojo/urls.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import importlib
|
2
|
+
import os
|
3
|
+
from django.urls import path, include
|
4
|
+
from mojo.helpers.settings import settings
|
5
|
+
from mojo.helpers import modules
|
6
|
+
|
7
|
+
MOJO_API_MODULE = settings.get("MOJO_API_MODULE", "rest")
|
8
|
+
|
9
|
+
urlpatterns = []
|
10
|
+
|
11
|
+
def load_mojo_modules():
|
12
|
+
# load the module to load its patterns
|
13
|
+
rest_module = modules.load_module(f"mojo.{MOJO_API_MODULE}", ignore_errors=False)
|
14
|
+
add_urlpatterns("mojo", prefix="")
|
15
|
+
|
16
|
+
for app in settings.INSTALLED_APPS:
|
17
|
+
module_name = f"{app}.{MOJO_API_MODULE}"
|
18
|
+
if not modules.module_exists(module_name):
|
19
|
+
continue
|
20
|
+
rest_module = modules.load_module(module_name, ignore_errors=False)
|
21
|
+
if rest_module:
|
22
|
+
app_name = app
|
23
|
+
if "." in app:
|
24
|
+
app_name = app.split('.')[-1]
|
25
|
+
prefix = getattr(rest_module, 'APP_NAME', app_name)
|
26
|
+
add_urlpatterns(app, prefix)
|
27
|
+
|
28
|
+
def add_urlpatterns(app, prefix):
|
29
|
+
app_module = modules.load_module(app)
|
30
|
+
if len(prefix) > 1:
|
31
|
+
prefix += "/"
|
32
|
+
if not hasattr(app_module, "urlpatterns"):
|
33
|
+
print(f"{app} has no api routes")
|
34
|
+
return
|
35
|
+
urls = path(prefix, include(app_module))
|
36
|
+
urlpatterns.append(urls)
|
37
|
+
|
38
|
+
load_mojo_modules()
|
mojo/ws4redis/README.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# Websocket HOWTO
|
2
|
+
|
3
|
+
## Authentication
|
4
|
+
|
5
|
+
### JWT
|
6
|
+
|
7
|
+
Requires an existing JWT token that has gone through authentication process via rest
|
8
|
+
|
9
|
+
```json
|
10
|
+
{
|
11
|
+
"action": "auth",
|
12
|
+
"kind": "jwt",
|
13
|
+
"token": "..."
|
14
|
+
}
|
15
|
+
```
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
### Model Authentication
|
20
|
+
|
21
|
+
You can implement custom authentication flows via a model by using the WS4REDIS_AUTHENTICATORS in your django settings.py.
|
22
|
+
|
23
|
+
##### WS4REDIS_AUTHENTICATORS
|
24
|
+
|
25
|
+
```python
|
26
|
+
WS4REDIS_AUTHENTICATORS = {
|
27
|
+
"mymodel": "myapp.MyModel"
|
28
|
+
}
|
29
|
+
```
|
30
|
+
|
31
|
+
In your Model you will need to add the following class methods.
|
32
|
+
|
33
|
+
This method is used by the async/websocket service to authenticate.
|
34
|
+
If the model can authenticate the connection it should return dict with kind and pk of the model that is authenticaed.
|
35
|
+
|
36
|
+
|
37
|
+
|
38
|
+
##### authWS4RedisConnection
|
39
|
+
|
40
|
+
This method will authenticate the model, or return None if authentication failed.
|
41
|
+
|
42
|
+
```python
|
43
|
+
@classmethod
|
44
|
+
def authWS4RedisConnection(cls, auth_data):
|
45
|
+
if auth_data and auth_data.token:
|
46
|
+
terminal = cls.objects.filter(token=auth_data.token).last()
|
47
|
+
if terminal is not None:
|
48
|
+
# we now return the terminal credentials to the framework
|
49
|
+
return UberDict(
|
50
|
+
kind="terminal",
|
51
|
+
pk=terminal.id,
|
52
|
+
uuid=terminal.tid,
|
53
|
+
token=auth_data.token,
|
54
|
+
only_one=True, # only allows one connection at a time
|
55
|
+
instance=terminal)
|
56
|
+
return None
|
57
|
+
```
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
##### canPublishTo
|
62
|
+
|
63
|
+
Add this to your Model to validate messages from this connection to be sent to this channel.
|
64
|
+
|
65
|
+
```python
|
66
|
+
@classmethod
|
67
|
+
def canPublishTo(cls, credentials, msg):
|
68
|
+
if credentials:
|
69
|
+
return True
|
70
|
+
return False
|
71
|
+
```
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
##### WS4REDIS_CHANNELS
|
76
|
+
|
77
|
+
Map channels to models
|
78
|
+
|
79
|
+
```python
|
80
|
+
WS4REDIS_CHANNELS = {
|
81
|
+
"group": "account.Group",
|
82
|
+
"chat": "chat.Room",
|
83
|
+
}
|
84
|
+
```
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
##### onWS4RedisMessage
|
89
|
+
|
90
|
+
Add this to your Model to allow for handling of messages sent to this channel.
|
91
|
+
|
92
|
+
```python
|
93
|
+
@classmethod
|
94
|
+
def onWS4RedisMessage(cls, credentials, msg):
|
95
|
+
if msg.action == "status":
|
96
|
+
cls.createStatusRecord(msg)
|
97
|
+
|
98
|
+
```
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
### URL Params
|
103
|
+
|
104
|
+
You can also use params in the url of the websocket.
|
105
|
+
|
106
|
+
**THIS IS NOT RECOMMENDED as the url params are not encrypted and can be easily snooped.**
|
107
|
+
|
108
|
+
Include something like the follow in your django settings.py:
|
109
|
+
|
110
|
+
```python
|
111
|
+
def URL_AUTHENTICATOR(ws_con):
|
112
|
+
from objict import objict
|
113
|
+
token = ws_con.request.GET.get("token", None)
|
114
|
+
session_key = ws_con.request.GET.get("session_key", None)
|
115
|
+
if token is not None:
|
116
|
+
# this example assume the token is used for terminal auth
|
117
|
+
# you will still need to implement the Custom Auth flows to handle this
|
118
|
+
ws_con.on_auth(objict(kind="terminal", token=token))
|
119
|
+
elif session_key is not None:
|
120
|
+
# or alternative is a session
|
121
|
+
ws_con.on_auth(objict(kind="session", token=session_key))
|
122
|
+
|
123
|
+
```
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
## Subscribe
|
128
|
+
|
129
|
+
```json
|
130
|
+
{
|
131
|
+
"action": "subscribe",
|
132
|
+
"channel": "group",
|
133
|
+
"pk": 3,
|
134
|
+
}
|
135
|
+
```
|
136
|
+
|
137
|
+
### Security
|
138
|
+
|
139
|
+
In settins WS4REDIS_CHANNELS, map your channel to a model.
|
140
|
+
The model should have a classmethod for canSubscribeTo that returns a list of pk they can subscribe to.
|
141
|
+
|
142
|
+
|
143
|
+
## UnSubscribe
|
144
|
+
|
145
|
+
```json
|
146
|
+
{
|
147
|
+
"action": "unsubscribe",
|
148
|
+
"channel": "group",
|
149
|
+
"pk": 3,
|
150
|
+
}
|
151
|
+
```
|
152
|
+
|
153
|
+
|
154
|
+
## Publish / Send To
|
155
|
+
|
156
|
+
```json
|
157
|
+
{
|
158
|
+
"action": "publish",
|
159
|
+
"channel": "group",
|
160
|
+
"pk": 3,
|
161
|
+
"message": "..."
|
162
|
+
}
|
163
|
+
```
|
164
|
+
|
165
|
+
### Security
|
166
|
+
|
167
|
+
In settins WS4REDIS_CHANNELS, map your channel to a model.
|
168
|
+
The model should have a classmethod for canPublishTo that returns a list of pk they can publish to.
|
169
|
+
|
170
|
+
|
171
|
+
## Custom Messages
|
172
|
+
|
173
|
+
If an unknown action is sent with a channel then the framework will call onWS4RedisMessage on the channel model.
|
174
|
+
|