django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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 (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
  2. django_nativemojo-0.1.17.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +279 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -0,0 +1,388 @@
1
+ # Django-MOJO Serializer: Suggested Performance Improvements
2
+
3
+ This document outlines a concrete plan to further optimize serialization and database fetching for the Django-MOJO serializers, with an emphasis on the `OptimizedGraphSerializer`. The goal is to reduce CPU overhead, minimize ORM round-trips, and leverage database features where possible, all while maintaining full backward compatibility.
4
+
5
+ -------------------------------------------------------------------------------
6
+
7
+ ## Goals
8
+
9
+ - Reduce per-object overhead during serialization
10
+ - Eliminate repeated graph resolution and _meta lookups in hot paths
11
+ - Optimize QuerySet fetching patterns (select_related, prefetch_related, only)
12
+ - Introduce a `values()`-based fast path for "simple graphs" that need only model fields
13
+ - Preserve functional correctness, optional nested graphs, and extras
14
+ - Keep improved behaviors behind feature flags where needed
15
+
16
+ -------------------------------------------------------------------------------
17
+
18
+ ## Summary of Observed Bottlenecks
19
+
20
+ - Repeated graph config parsing per object
21
+ - Frequent `_meta.get_field` calls inside tight loops
22
+ - Query optimizations applied ad hoc (per call) without caching the plan
23
+ - Attribute access and Python-side transformations dominating CPU time
24
+ - Lost opportunities to use `.values()` fast path when no nested graphs/extras are needed
25
+ - Repeated serialization of the same related objects across a single request (now improved with request cache)
26
+
27
+ -------------------------------------------------------------------------------
28
+
29
+ ## Proposed Improvements
30
+
31
+ ### 1) Graph Compilation with LRU Cache
32
+
33
+ Compile a model’s graph once per `(model_class, graph_name)` and cache the result. The compilation should produce:
34
+
35
+ - Resolved fields list (fallback to all model fields if not specified)
36
+ - Extras normalized to a list of (method_name, alias) tuples
37
+ - Related graphs map
38
+ - Pre-resolved FK/OneToOne field names set (for fast PK extraction)
39
+ - Prebuilt attribute getters for nested dot paths in extras (via `operator.attrgetter`)
40
+ - A reusable “query plan” (see #2)
41
+
42
+ Benefits:
43
+ - Eliminates repeated RestMeta/graph lookups
44
+ - Eliminates per-field _meta calls in hot loops
45
+ - Provides a single source of truth for fetching and serialization
46
+
47
+ Implementation notes:
48
+ - Use `functools.lru_cache(maxsize=512)` for `compile_graph(model_class, graph_name)`
49
+ - Normalize cases where graph exists but has no `fields` (serialize all model fields)
50
+
51
+ Configuration:
52
+ - MOJO_OPTIMIZED_COMPILE_CACHE_MAXSIZE (default: 512)
53
+
54
+
55
+ ### 2) Query Plan Caching and Application
56
+
57
+ Attach a query plan to the compiled graph that includes:
58
+ - `select_related` fields (FK and O2O discovered in fields and related graphs)
59
+ - `prefetch_related` fields (M2M and reverse relations)
60
+ - `only()` field list for the main model (minimize columns fetched)
61
+ - Optional `Prefetch()` for related sets with `.only()` tuning
62
+
63
+ Apply the query plan once on the collection queryset before evaluation.
64
+
65
+ Benefits:
66
+ - Minimizes N+1 queries
67
+ - Reduces column payload
68
+ - Centralizes fetching strategy for consistent performance
69
+
70
+ Configuration:
71
+ - MOJO_OPTIMIZED_APPLY_QUERY_PLAN = True
72
+ - MOJO_OPTIMIZED_ONLY_FIELDS = True
73
+
74
+
75
+ ### 3) `.values()` Fast Path for Simple Graphs
76
+
77
+ When a graph has:
78
+ - No extras
79
+ - No related graphs
80
+ - Only plain model fields (no callables)
81
+
82
+ Then use `QuerySet.values(*fields)` and return rows directly, with minimal post-processing (datetime/decimal handling as needed).
83
+
84
+ Benefits:
85
+ - Skips model instance creation and attribute access
86
+ - Significantly faster for large lists
87
+
88
+ Notes:
89
+ - If you later add extras or nested graphs, the fast path must be bypassed automatically
90
+
91
+ Configuration:
92
+ - MOJO_OPTIMIZED_VALUES_FASTPATH = True
93
+
94
+
95
+ ### 4) Share a Single Request Cache Across Nested Serialization
96
+
97
+ Ensure nested serializers share the same request-scoped cache dict, so repeated related objects are serialized once per request.
98
+
99
+ Benefits:
100
+ - Prevents re-serializing the same related object multiple times within a request
101
+ - Big wins for graphs with repeated authors/users/tags/etc.
102
+
103
+
104
+ ### 5) Hot Loop Optimizations
105
+
106
+ - Inline FK check via the compiled plan’s `fk_fields` set
107
+ - Bind local variables outside loops (e.g., `svf = self._serialize_value_fast`, `data = {}; fields = plan['fields']`) to reduce attribute lookups
108
+ - Avoid `_meta.get_field` in loops (rely on the compiled plan)
109
+ - Return early when values are None/unset as appropriate
110
+
111
+ Benefits:
112
+ - Small per-iteration savings amplify at 1k+ objects
113
+
114
+
115
+ ### 6) Extras Handling and DB Annotations
116
+
117
+ For simple extras (counts, derived numbers) that can be expressed via annotations:
118
+ - Prefer `.annotate()` and include them in `values()` (for the fast path) or in `.only()` plan for instance path
119
+ - For method-based extras, keep current behavior but consider caching per object in the request cache if deterministic
120
+
121
+ Examples:
122
+ - `post_count` => `.annotate(post_count=Count('posts'))`
123
+ - `full_name` for `User` => annotate `Concat(F('first_name'), Value(' '), F('last_name'))` when appropriate
124
+
125
+ Configuration:
126
+ - MOJO_OPTIMIZED_ALLOW_ANNOTATIONS = True
127
+
128
+
129
+ ### 7) Iterator vs List Strategy
130
+
131
+ - Default: use `list(qs)` which is fastest in typical scenarios when memory allows
132
+ - For very large datasets, provide an option to stream via `iterator(chunk_size=...)`
133
+ - When using `.values()` fast path, the memory footprint is smaller; streaming remains available
134
+
135
+ Configuration:
136
+ - MOJO_OPTIMIZED_LIST_EVALUATION = True
137
+ - MOJO_OPTIMIZED_ITERATOR_CHUNK_SIZE = 2000
138
+
139
+
140
+ ### 8) JSON Serialization Path
141
+
142
+ - Keep `ujson` (orjson if available in future) for hot paths
143
+ - Avoid pretty-printing in normal responses
144
+ - `ensure_ascii=False`
145
+ - Provide a per-response toggle for tooling/debugging
146
+
147
+ Configuration:
148
+ - MOJO_JSON_USE_UJSON = True
149
+ - MOJO_JSON_PRETTY_DEBUG = False
150
+
151
+
152
+ ### 9) Logging and Metrics
153
+
154
+ - Keep logging at WARNING or above in hot paths
155
+ - Add lightweight counters (per process) for:
156
+ - How many serializations used values fast path
157
+ - Cache hit/miss for request-scoped cache (approximate)
158
+ - Expose a `get_performance_info()` snapshot on serializer instances (already present; keep it cheap)
159
+
160
+ Configuration:
161
+ - MOJO_OPTIMIZED_COLLECT_METRICS = True
162
+
163
+
164
+ ### 10) Feature Flags and Backwards Compatibility
165
+
166
+ - All new behavior guarded by settings flags (enabled by default where safe)
167
+ - Maintain previous public API
168
+ - Fallback paths for missing `RestMeta` or graphs:
169
+ - If graph not found => fallback to `default`
170
+ - If no RestMeta/default => serialize all fields (current behavior, keep)
171
+
172
+ -------------------------------------------------------------------------------
173
+
174
+ ## Design Sketches (Pseudo-code)
175
+
176
+ Graph compilation:
177
+
178
+ @lru_cache(maxsize=512)
179
+ def compile_graph(model_class, graph_name):
180
+ graphs = getattr(getattr(model_class, 'RestMeta', None), 'GRAPHS', {}) or {}
181
+ gc = graphs.get(graph_name) or graphs.get('default') or {}
182
+ fields = gc.get('fields') or [f.name for f in model_class._meta.fields]
183
+ extras = normalize_extras(gc.get('extra', [])) # list of (method, alias)
184
+ related_graphs = gc.get('graphs', {})
185
+ fk_fields = detect_fk_fields(model_class, fields)
186
+ extra_getters = build_attrgetters_for_dotted_paths(extras)
187
+ plan = build_query_plan(model_class, fields, related_graphs)
188
+ return {
189
+ 'fields': fields,
190
+ 'extras': extras,
191
+ 'related_graphs': related_graphs,
192
+ 'fk_fields': fk_fields,
193
+ 'extra_getters': extra_getters,
194
+ 'plan': plan,
195
+ }
196
+
197
+ Values fast path:
198
+
199
+ def can_use_values_fastpath(compiled):
200
+ return not compiled['extras'] and not compiled['related_graphs']
201
+
202
+ def serialize_queryset_fast_values(qs, compiled):
203
+ qs = apply_only(qs, compiled['plan'])
204
+ rows = list(qs.values(*compiled['fields']))
205
+ return postprocess_rows(rows) # datetime/decimal fixups
206
+
207
+ Applying query plan:
208
+
209
+ def apply_query_plan(qs, compiled):
210
+ plan = compiled['plan']
211
+ if plan['select_related']: qs = qs.select_related(*plan['select_related'])
212
+ if plan['prefetch_related']: qs = qs.prefetch_related(*plan['prefetch_related'])
213
+ if plan['only_fields']: qs = qs.only(*plan['only_fields'])
214
+ return qs
215
+
216
+ Instance hot loop:
217
+
218
+ def serialize_instance_with_compiled(obj, compiled, request_cache):
219
+ data = {}
220
+ svf = self._serialize_value_fast
221
+ for fname in compiled['fields']:
222
+ val = getattr(obj, fname, None)
223
+ if fname in compiled['fk_fields'] and hasattr(val, 'pk'):
224
+ data[fname] = val.pk
225
+ else:
226
+ if callable(val):
227
+ try: val = val()
228
+ except: val = None
229
+ data[fname] = svf(val)
230
+ for (method_name, alias) in compiled['extras']:
231
+ val = resolve_extra_value(obj, method_name, compiled['extra_getters'])
232
+ data[alias] = svf(val)
233
+ for related_name, sub_graph in compiled['related_graphs'].items():
234
+ data[related_name] = serialize_related(obj, related_name, sub_graph, request_cache)
235
+ return data
236
+
237
+ -------------------------------------------------------------------------------
238
+
239
+ ## Should We Patch the Existing Serializer or Add a New One?
240
+
241
+ Recommendation: Patch the existing `OptimizedGraphSerializer` incrementally behind feature flags.
242
+
243
+ Pros:
244
+ - Avoids registry complexity and code duplication
245
+ - Keeps a single optimized path maintained and tested
246
+ - We can toggle new behaviors in production via settings
247
+ - Existing API and behavior preserved (with improved performance)
248
+
249
+ When to consider a new serializer:
250
+ - If you want an experimental “v2” without any risk to current behavior
251
+ - If dramatic API changes (not planned here) are desired
252
+
253
+ Compromise approach:
254
+ - Keep changes in `OptimizedGraphSerializer`
255
+ - Add a “strict fast mode” flag (e.g., `simple_mode=True`) for safe fallbacks during rollout
256
+ - If needed, register an alias “optimized_v2” pointing to the same class but with different default flags, selectable via settings/manager
257
+
258
+ -------------------------------------------------------------------------------
259
+
260
+ ## Rollout Strategy
261
+
262
+ 1) Phase 1 (compile + plan):
263
+ - Introduce compile_graph() with LRU cache
264
+ - Attach and apply query plan (select_related/prefetch/only)
265
+ - Keep behavior identical (no values fast path yet)
266
+ - Flag: MOJO_OPTIMIZED_APPLY_QUERY_PLAN = True (default True)
267
+
268
+ 2) Phase 2 (values fast path):
269
+ - Implement can_use_values_fastpath() and serialize_queryset_fast_values()
270
+ - Enable for limited models/graphs (opt-in list)
271
+ - Flag: MOJO_OPTIMIZED_VALUES_FASTPATH = True (default True), with allowlist MOJO_VALUES_FASTPATH_ALLOW = []
272
+
273
+ 3) Phase 3 (extras annotations):
274
+ - Add optional annotation hooks
275
+ - Maintain correctness: fallback to Python extras if annotation not configured
276
+ - Flag: MOJO_OPTIMIZED_ALLOW_ANNOTATIONS = False (default off)
277
+
278
+ 4) Phase 4 (observability):
279
+ - Add counters for fast path usage and request cache stats
280
+ - Light HTTP headers or log lines during debug windows
281
+
282
+ -------------------------------------------------------------------------------
283
+
284
+ ## Testing Plan
285
+
286
+ - Unit tests:
287
+ - Graph compilation correctness (fields, extras, fk_fields, plan)
288
+ - Fallback behaviors when no RestMeta or empty graph fields
289
+ - Request-cache sharing across nested graphs
290
+ - Values fast path returns same data shape as instance path for simple graphs
291
+
292
+ - Integration tests:
293
+ - Paginated collections with and without nested graphs
294
+ - Sorting, only(), select_related and prefetch coverage
295
+ - Large datasets (e.g. 10k rows) for memory/time regression testing
296
+
297
+ - Performance tests:
298
+ - Compare simple vs advanced vs optimized across:
299
+ - simple fields-only graphs
300
+ - graphs with extras
301
+ - graphs with nested related graphs
302
+ - Vary dataset sizes (1k, 10k, 100k)
303
+
304
+ -------------------------------------------------------------------------------
305
+
306
+ ## Metrics and Diagnostics
307
+
308
+ - Counters (in-memory):
309
+ - optimized.values_fastpath_used (count)
310
+ - optimized.request_cache_hits/misses (approximate)
311
+ - Optional headers (debug only):
312
+ - X-Serializer-FastPath: values|instance
313
+ - X-Request-Cache: hits=N;misses=M
314
+ - Expose a `get_performance_info()` summary (already exists; keep cheap)
315
+
316
+ -------------------------------------------------------------------------------
317
+
318
+ ## Risks and Mitigations
319
+
320
+ - values() fast path may lose custom Python-only transformation:
321
+ - Only enable when no extras or nested graphs are present
322
+ - Optionally support DB annotations if needed
323
+ - only() could hide columns needed by extras:
324
+ - Start with fields-only; allow expanding via settings/annotations
325
+ - Prefetch can over-fetch if misconfigured:
326
+ - Cache plan per graph; keep minimal defaults, expand conservatively
327
+ - Memory pressure:
328
+ - Provide iterator chunk option; defaults to list-based for speed
329
+
330
+ -------------------------------------------------------------------------------
331
+
332
+ ## Configuration (Proposed)
333
+
334
+ - MOJO_OPTIMIZED_COMPILE_CACHE_MAXSIZE = 512
335
+ - MOJO_OPTIMIZED_APPLY_QUERY_PLAN = True
336
+ - MOJO_OPTIMIZED_ONLY_FIELDS = True
337
+ - MOJO_OPTIMIZED_VALUES_FASTPATH = True
338
+ - MOJO_VALUES_FASTPATH_ALLOW = [] (optional allowlist of model.graph pairs)
339
+ - MOJO_OPTIMIZED_ALLOW_ANNOTATIONS = False
340
+ - MOJO_OPTIMIZED_LIST_EVALUATION = True
341
+ - MOJO_OPTIMIZED_ITERATOR_CHUNK_SIZE = 2000
342
+ - MOJO_OPTIMIZED_COLLECT_METRICS = True
343
+ - MOJO_JSON_USE_UJSON = True
344
+ - MOJO_JSON_PRETTY_DEBUG = False
345
+
346
+ -------------------------------------------------------------------------------
347
+
348
+ ## Acceptance Criteria
349
+
350
+ - For simple fields-only graphs, optimized serializer outperforms advanced by at least 30%
351
+ - For graphs with common repeated relationships, request-scoped caching yields 2x+ speedups vs simple
352
+ - Functional parity with existing behavior (fallbacks still work)
353
+ - No regressions in correctness verified by tests
354
+ - Feature flags allow safe rollback
355
+
356
+ -------------------------------------------------------------------------------
357
+
358
+ ## Proposed Timeline
359
+
360
+ - Week 1:
361
+ - Implement compile_graph() + query plan
362
+ - Unit tests and initial benchmarks
363
+ - Week 2:
364
+ - Implement values() fast path + allowlist
365
+ - Add request cache sharing guards + metrics
366
+ - Benchmarks across datasets
367
+ - Week 3:
368
+ - Optional DB annotations for extras
369
+ - Documentation and rollout
370
+ - Enable fast path by default for safe graphs
371
+
372
+ -------------------------------------------------------------------------------
373
+
374
+ ## Action Items
375
+
376
+ - [ ] Add compile_graph() (LRU-cached) and query plan builder
377
+ - [ ] Wire query plan in serialize() for QuerySets
378
+ - [ ] Implement values fast path detection + execution
379
+ - [ ] Ensure related nested serializations share the request cache
380
+ - [ ] Add feature flags and minimal metrics
381
+ - [ ] Expand unit tests and integration tests
382
+ - [ ] Document configuration and rollout steps
383
+
384
+ -------------------------------------------------------------------------------
385
+
386
+ ## Conclusion
387
+
388
+ By moving to a compiled-graph model, caching the fetch plan, and using a `values()` fast path for simple graphs, we can significantly reduce CPU and DB overhead while preserving the current API and functionality. The improvements are incremental, reversible via feature flags, and deliver measurable performance gains across common workloads.
testit/client.py CHANGED
@@ -61,7 +61,7 @@ class RestClient:
61
61
  response = requests.request(method, url, headers=headers, **kwargs)
62
62
  if self.logger:
63
63
  self.logger.info("REQUEST", f"{method}:{url}", headers)
64
- self.logger.info(kwargs.get("json", ""))
64
+ self.logger.info("params:",kwargs.get("params", ""), "json:", kwargs.get("json", ""))
65
65
  try:
66
66
  data = objict.fromdict(response.json()) if response.content else None
67
67
  response_data = objict(response=data, status_code=response.status_code)
testit/helpers.py CHANGED
@@ -206,10 +206,24 @@ def get_mock_user():
206
206
  Returns:
207
207
  objict: A mock user object with basic attributes.
208
208
  """
209
+ from mojo.helpers import crypto
209
210
  user = objict()
210
211
  user.id = 1
211
212
  user.username = "mockuser"
212
213
  user.email = "mockuser@example.com"
213
214
  user.is_authenticated = True
215
+ user.password = crypto.random_string(16)
214
216
  user.has_permission = lambda perm: True
215
217
  return user
218
+
219
+ def get_admin_user():
220
+ """
221
+ Creates a mock admin user object.
222
+
223
+ Returns:
224
+ objict: A mock admin user object with basic attributes.
225
+ """
226
+ user = get_mock_user()
227
+ user.is_superuser = True
228
+ user.is_staff = True
229
+ return user
testit/runner.py CHANGED
@@ -45,8 +45,8 @@ def setup_parser():
45
45
  help="Run only this app/module")
46
46
  parser.add_argument("--method", type=str, default=None,
47
47
  help="Run only a specific test method")
48
- parser.add_argument("-t", "--test", type=str, default=None,
49
- help="Specify a specific test method to run")
48
+ parser.add_argument("-t", "--test", action="append", dest="test_modules",
49
+ help="Specify test modules or specific tests (can be used multiple times)")
50
50
  parser.add_argument("-q", "--quick", action="store_true",
51
51
  help="Run only tests flagged as critical/quick")
52
52
  parser.add_argument("-x", "--extra", type=str, default=None,
@@ -65,6 +65,8 @@ def setup_parser():
65
65
  help="Do not run Mojo app tests")
66
66
  parser.add_argument("--onlymojo", action="store_true",
67
67
  help="Only run Mojo app tests")
68
+ parser.add_argument("--ignore", action="append", dest="ignore_modules",
69
+ help="Ignore specific test modules (can be used multiple times)")
68
70
  return parser.parse_args()
69
71
 
70
72
 
@@ -144,7 +146,8 @@ def run_module_setup(opts, module, test_name, module_name):
144
146
 
145
147
 
146
148
  def run_module_tests(opts, module, test_name, module_name):
147
- opts.client = testit.client.RestClient(opts.host, logger=opts.logger)
149
+ if not getattr(opts, 'client', None):
150
+ opts.client = testit.client.RestClient(opts.host, logger=opts.logger)
148
151
  test_key = f"{module_name}.{test_name}"
149
152
  logit.color_print(f"\nRUNNING TEST: {test_key}", logit.ConsoleLogger.BLUE)
150
153
  started = time.time()
@@ -225,19 +228,33 @@ def main(opts):
225
228
  sys.path.insert(0, parent_test_root)
226
229
  parent_test_modules = sorted([d for d in os.listdir(parent_test_root) if os.path.isdir(os.path.join(parent_test_root, d))])
227
230
 
228
- if opts.module and opts.test:
231
+ # Handle multiple --test arguments
232
+ if opts.test_modules:
233
+ for test_spec in opts.test_modules:
234
+ if '.' in test_spec:
235
+ module_name, test_name = test_spec.split('.', 1)
236
+ run_module_tests_by_name(opts, module_name, test_name)
237
+ else:
238
+ run_tests_for_module(opts, test_spec, TEST_ROOT, parent_test_root)
239
+ elif opts.module and opts.test:
229
240
  run_module_tests_by_name(opts, opts.module, opts.test)
230
241
  elif opts.module:
231
242
  run_tests_for_module(opts, opts.module, TEST_ROOT, parent_test_root)
232
243
  else:
233
244
  test_root = os.path.join(paths.APPS_ROOT, "tests")
234
245
  test_modules = sorted([d for d in os.listdir(test_root) if os.path.isdir(os.path.join(test_root, d))])
246
+
247
+ # Filter out ignored modules
248
+ ignored_modules = opts.ignore_modules or []
249
+
235
250
  if parent_test_modules and not opts.nomojo:
236
251
  for module_name in parent_test_modules:
237
- run_tests_for_module(opts, module_name, parent_test_root)
252
+ if module_name not in ignored_modules:
253
+ run_tests_for_module(opts, module_name, parent_test_root)
238
254
  if not opts.onlymojo:
239
255
  for module_name in test_modules:
240
- run_tests_for_module(opts, module_name, test_root)
256
+ if module_name not in ignored_modules:
257
+ run_tests_for_module(opts, module_name, test_root)
241
258
 
242
259
  # Summary Output
243
260
  print("\n" + "=" * 80)