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.
Files changed (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. 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()
@@ -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
+
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+ __version__ = '1.1.8'