django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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 (276) hide show
  1. django_nativemojo-0.1.16.dist-info/METADATA +138 -0
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +651 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  10. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  11. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  12. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  13. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  14. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  15. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  16. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  17. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  18. mojo/apps/account/models/__init__.py +2 -0
  19. mojo/apps/account/models/device.py +281 -0
  20. mojo/apps/account/models/group.py +319 -15
  21. mojo/apps/account/models/member.py +29 -5
  22. mojo/apps/account/models/push/__init__.py +4 -0
  23. mojo/apps/account/models/push/config.py +112 -0
  24. mojo/apps/account/models/push/delivery.py +93 -0
  25. mojo/apps/account/models/push/device.py +66 -0
  26. mojo/apps/account/models/push/template.py +99 -0
  27. mojo/apps/account/models/user.py +369 -19
  28. mojo/apps/account/rest/__init__.py +2 -0
  29. mojo/apps/account/rest/device.py +39 -0
  30. mojo/apps/account/rest/group.py +9 -0
  31. mojo/apps/account/rest/push.py +187 -0
  32. mojo/apps/account/rest/user.py +100 -6
  33. mojo/apps/account/services/__init__.py +1 -0
  34. mojo/apps/account/services/push.py +363 -0
  35. mojo/apps/aws/migrations/0001_initial.py +206 -0
  36. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  37. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  38. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  39. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  40. mojo/apps/aws/models/__init__.py +19 -0
  41. mojo/apps/aws/models/email_attachment.py +99 -0
  42. mojo/apps/aws/models/email_domain.py +218 -0
  43. mojo/apps/aws/models/email_template.py +132 -0
  44. mojo/apps/aws/models/incoming_email.py +197 -0
  45. mojo/apps/aws/models/mailbox.py +288 -0
  46. mojo/apps/aws/models/sent_message.py +175 -0
  47. mojo/apps/aws/rest/__init__.py +7 -0
  48. mojo/apps/aws/rest/email.py +33 -0
  49. mojo/apps/aws/rest/email_ops.py +183 -0
  50. mojo/apps/aws/rest/messages.py +32 -0
  51. mojo/apps/aws/rest/s3.py +64 -0
  52. mojo/apps/aws/rest/send.py +101 -0
  53. mojo/apps/aws/rest/sns.py +403 -0
  54. mojo/apps/aws/rest/templates.py +19 -0
  55. mojo/apps/aws/services/__init__.py +32 -0
  56. mojo/apps/aws/services/email.py +390 -0
  57. mojo/apps/aws/services/email_ops.py +548 -0
  58. mojo/apps/docit/__init__.py +6 -0
  59. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  60. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  61. mojo/apps/docit/migrations/0001_initial.py +113 -0
  62. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  63. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  64. mojo/apps/docit/models/__init__.py +17 -0
  65. mojo/apps/docit/models/asset.py +231 -0
  66. mojo/apps/docit/models/book.py +227 -0
  67. mojo/apps/docit/models/page.py +319 -0
  68. mojo/apps/docit/models/page_revision.py +203 -0
  69. mojo/apps/docit/rest/__init__.py +10 -0
  70. mojo/apps/docit/rest/asset.py +17 -0
  71. mojo/apps/docit/rest/book.py +22 -0
  72. mojo/apps/docit/rest/page.py +22 -0
  73. mojo/apps/docit/rest/page_revision.py +17 -0
  74. mojo/apps/docit/services/__init__.py +11 -0
  75. mojo/apps/docit/services/docit.py +315 -0
  76. mojo/apps/docit/services/markdown.py +44 -0
  77. mojo/apps/fileman/README.md +8 -8
  78. mojo/apps/fileman/backends/base.py +76 -70
  79. mojo/apps/fileman/backends/filesystem.py +86 -86
  80. mojo/apps/fileman/backends/s3.py +409 -108
  81. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  82. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  83. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  84. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  85. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  86. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  87. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  88. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  89. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  90. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  91. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  92. mojo/apps/fileman/models/__init__.py +1 -5
  93. mojo/apps/fileman/models/file.py +240 -58
  94. mojo/apps/fileman/models/manager.py +427 -31
  95. mojo/apps/fileman/models/rendition.py +118 -0
  96. mojo/apps/fileman/renderer/__init__.py +111 -0
  97. mojo/apps/fileman/renderer/audio.py +403 -0
  98. mojo/apps/fileman/renderer/base.py +205 -0
  99. mojo/apps/fileman/renderer/document.py +404 -0
  100. mojo/apps/fileman/renderer/image.py +222 -0
  101. mojo/apps/fileman/renderer/utils.py +297 -0
  102. mojo/apps/fileman/renderer/video.py +304 -0
  103. mojo/apps/fileman/rest/__init__.py +1 -18
  104. mojo/apps/fileman/rest/upload.py +22 -32
  105. mojo/apps/fileman/signals.py +58 -0
  106. mojo/apps/fileman/tasks.py +254 -0
  107. mojo/apps/fileman/utils/__init__.py +40 -16
  108. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  109. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  110. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  111. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  112. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  113. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  114. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  115. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  116. mojo/apps/incident/models/__init__.py +2 -0
  117. mojo/apps/incident/models/event.py +35 -0
  118. mojo/apps/incident/models/history.py +36 -0
  119. mojo/apps/incident/models/incident.py +3 -1
  120. mojo/apps/incident/models/ticket.py +62 -0
  121. mojo/apps/incident/reporter.py +21 -1
  122. mojo/apps/incident/rest/__init__.py +1 -0
  123. mojo/apps/incident/rest/event.py +7 -1
  124. mojo/apps/incident/rest/ticket.py +43 -0
  125. mojo/apps/jobs/__init__.py +489 -0
  126. mojo/apps/jobs/adapters.py +24 -0
  127. mojo/apps/jobs/cli.py +616 -0
  128. mojo/apps/jobs/daemon.py +370 -0
  129. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  130. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  131. mojo/apps/jobs/handlers/__init__.py +5 -0
  132. mojo/apps/jobs/handlers/webhook.py +317 -0
  133. mojo/apps/jobs/job_engine.py +734 -0
  134. mojo/apps/jobs/keys.py +203 -0
  135. mojo/apps/jobs/local_queue.py +363 -0
  136. mojo/apps/jobs/management/__init__.py +3 -0
  137. mojo/apps/jobs/management/commands/__init__.py +3 -0
  138. mojo/apps/jobs/manager.py +1327 -0
  139. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  140. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  141. mojo/apps/jobs/models/__init__.py +6 -0
  142. mojo/apps/jobs/models/job.py +441 -0
  143. mojo/apps/jobs/rest/__init__.py +2 -0
  144. mojo/apps/jobs/rest/control.py +466 -0
  145. mojo/apps/jobs/rest/jobs.py +421 -0
  146. mojo/apps/jobs/scheduler.py +571 -0
  147. mojo/apps/jobs/services/__init__.py +6 -0
  148. mojo/apps/jobs/services/job_actions.py +465 -0
  149. mojo/apps/jobs/settings.py +209 -0
  150. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  151. mojo/apps/logit/models/log.py +7 -1
  152. mojo/apps/metrics/__init__.py +8 -1
  153. mojo/apps/metrics/redis_metrics.py +198 -0
  154. mojo/apps/metrics/rest/__init__.py +3 -0
  155. mojo/apps/metrics/rest/categories.py +266 -0
  156. mojo/apps/metrics/rest/helpers.py +48 -0
  157. mojo/apps/metrics/rest/permissions.py +99 -0
  158. mojo/apps/metrics/rest/values.py +277 -0
  159. mojo/apps/metrics/utils.py +19 -2
  160. mojo/decorators/auth.py +6 -1
  161. mojo/decorators/http.py +47 -3
  162. mojo/helpers/aws/__init__.py +45 -0
  163. mojo/helpers/aws/ec2.py +804 -0
  164. mojo/helpers/aws/iam.py +748 -0
  165. mojo/helpers/aws/inbound_email.py +309 -0
  166. mojo/helpers/aws/kms.py +413 -0
  167. mojo/helpers/aws/s3.py +451 -11
  168. mojo/helpers/aws/ses.py +483 -0
  169. mojo/helpers/aws/ses_domain.py +959 -0
  170. mojo/helpers/aws/sns.py +461 -0
  171. mojo/helpers/crypto/__init__.py +1 -1
  172. mojo/helpers/crypto/utils.py +15 -0
  173. mojo/helpers/dates.py +18 -0
  174. mojo/helpers/location/__init__.py +2 -0
  175. mojo/helpers/location/countries.py +262 -0
  176. mojo/helpers/location/geolocation.py +196 -0
  177. mojo/helpers/logit.py +37 -0
  178. mojo/helpers/redis/__init__.py +2 -0
  179. mojo/helpers/redis/adapter.py +606 -0
  180. mojo/helpers/redis/client.py +48 -0
  181. mojo/helpers/redis/pool.py +225 -0
  182. mojo/helpers/request.py +8 -0
  183. mojo/helpers/response.py +14 -2
  184. mojo/helpers/settings/__init__.py +2 -0
  185. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  186. mojo/helpers/settings/parser.py +132 -0
  187. mojo/middleware/auth.py +1 -1
  188. mojo/middleware/cors.py +40 -0
  189. mojo/middleware/logging.py +131 -12
  190. mojo/middleware/mojo.py +10 -0
  191. mojo/models/rest.py +494 -65
  192. mojo/models/secrets.py +98 -3
  193. mojo/serializers/__init__.py +106 -0
  194. mojo/serializers/core/__init__.py +90 -0
  195. mojo/serializers/core/cache/__init__.py +121 -0
  196. mojo/serializers/core/cache/backends.py +518 -0
  197. mojo/serializers/core/cache/base.py +102 -0
  198. mojo/serializers/core/cache/disabled.py +181 -0
  199. mojo/serializers/core/cache/memory.py +287 -0
  200. mojo/serializers/core/cache/redis.py +533 -0
  201. mojo/serializers/core/cache/utils.py +454 -0
  202. mojo/serializers/core/manager.py +550 -0
  203. mojo/serializers/core/serializer.py +475 -0
  204. mojo/serializers/examples/settings.py +322 -0
  205. mojo/serializers/formats/csv.py +393 -0
  206. mojo/serializers/formats/localizers.py +509 -0
  207. mojo/serializers/{models.py → simple.py} +38 -15
  208. mojo/serializers/suggested_improvements.md +388 -0
  209. testit/client.py +1 -1
  210. testit/helpers.py +35 -4
  211. testit/runner.py +23 -6
  212. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  213. django_nativemojo-0.1.10.dist-info/RECORD +0 -194
  214. mojo/apps/metrics/rest/db.py +0 -0
  215. mojo/apps/notify/README.md +0 -91
  216. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  217. mojo/apps/notify/admin.py +0 -52
  218. mojo/apps/notify/handlers/example_handlers.py +0 -516
  219. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  220. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  221. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  222. mojo/apps/notify/handlers/ses/message.py +0 -86
  223. mojo/apps/notify/management/commands/__init__.py +0 -1
  224. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  225. mojo/apps/notify/mod +0 -0
  226. mojo/apps/notify/models/__init__.py +0 -12
  227. mojo/apps/notify/models/account.py +0 -128
  228. mojo/apps/notify/models/attachment.py +0 -24
  229. mojo/apps/notify/models/bounce.py +0 -68
  230. mojo/apps/notify/models/complaint.py +0 -40
  231. mojo/apps/notify/models/inbox.py +0 -113
  232. mojo/apps/notify/models/inbox_message.py +0 -173
  233. mojo/apps/notify/models/outbox.py +0 -129
  234. mojo/apps/notify/models/outbox_message.py +0 -288
  235. mojo/apps/notify/models/template.py +0 -30
  236. mojo/apps/notify/providers/aws.py +0 -73
  237. mojo/apps/notify/rest/ses.py +0 -0
  238. mojo/apps/notify/utils/__init__.py +0 -2
  239. mojo/apps/notify/utils/notifications.py +0 -404
  240. mojo/apps/notify/utils/parsing.py +0 -202
  241. mojo/apps/notify/utils/render.py +0 -144
  242. mojo/apps/tasks/README.md +0 -118
  243. mojo/apps/tasks/__init__.py +0 -11
  244. mojo/apps/tasks/manager.py +0 -489
  245. mojo/apps/tasks/rest/__init__.py +0 -2
  246. mojo/apps/tasks/rest/hooks.py +0 -0
  247. mojo/apps/tasks/rest/tasks.py +0 -62
  248. mojo/apps/tasks/runner.py +0 -174
  249. mojo/apps/tasks/tq_handlers.py +0 -14
  250. mojo/helpers/aws/setup_email.py +0 -0
  251. mojo/helpers/redis.py +0 -10
  252. mojo/models/meta.py +0 -262
  253. mojo/ws4redis/README.md +0 -174
  254. mojo/ws4redis/__init__.py +0 -2
  255. mojo/ws4redis/client.py +0 -283
  256. mojo/ws4redis/connection.py +0 -327
  257. mojo/ws4redis/exceptions.py +0 -32
  258. mojo/ws4redis/redis.py +0 -183
  259. mojo/ws4redis/servers/base.py +0 -86
  260. mojo/ws4redis/servers/django.py +0 -171
  261. mojo/ws4redis/servers/uwsgi.py +0 -63
  262. mojo/ws4redis/settings.py +0 -45
  263. mojo/ws4redis/utf8validator.py +0 -128
  264. mojo/ws4redis/websocket.py +0 -403
  265. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  266. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  267. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  268. /mojo/apps/{notify → aws}/__init__.py +0 -0
  269. /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
  270. /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
  271. /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
  272. /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
  273. /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
  274. /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
  275. /mojo/{serializers → rest}/openapi.py +0 -0
  276. /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,393 @@
1
+ import csv
2
+ import io
3
+ from decimal import Decimal
4
+ from datetime import datetime, date
5
+ from django.http import StreamingHttpResponse, HttpResponse
6
+ from django.db.models import QuerySet
7
+ from mojo.helpers import logit
8
+
9
+ logger = logit.get_logger("csv_formatter", "csv_formatter.log")
10
+
11
+
12
+ class CsvFormatter:
13
+ """
14
+ Advanced CSV formatter with streaming support and RestMeta.GRAPHS integration.
15
+ """
16
+
17
+ def __init__(self, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL,
18
+ encoding='utf-8', streaming_threshold=1000):
19
+ """
20
+ Initialize CSV formatter.
21
+
22
+ :param delimiter: Field delimiter (default comma)
23
+ :param quotechar: Quote character for fields containing special chars
24
+ :param quoting: Quoting behavior (csv.QUOTE_MINIMAL, etc.)
25
+ :param encoding: Character encoding for output
26
+ :param streaming_threshold: Minimum rows to trigger streaming response
27
+ """
28
+ self.delimiter = delimiter
29
+ self.quotechar = quotechar
30
+ self.quoting = quoting
31
+ self.encoding = encoding
32
+ self.streaming_threshold = streaming_threshold
33
+
34
+ def serialize_queryset(self, queryset, fields=None, graph=None, filename="export.csv",
35
+ headers=None, localize=None, stream=True):
36
+ """
37
+ Serialize a Django QuerySet to CSV format.
38
+
39
+ :param queryset: Django QuerySet to serialize
40
+ :param fields: List of field names or tuples (field_name, display_name)
41
+ :param graph: RestMeta graph name to use for field configuration
42
+ :param filename: Output filename
43
+ :param headers: Custom header names (overrides field names)
44
+ :param localize: Localization configuration
45
+ :param stream: Enable streaming for large datasets
46
+ :return: HttpResponse or StreamingHttpResponse
47
+ """
48
+ # Determine if we should stream based on queryset size
49
+ should_stream = stream and queryset.count() > self.streaming_threshold
50
+
51
+ # Get fields configuration
52
+ field_config = self._get_field_config(queryset, fields, graph)
53
+
54
+ if should_stream:
55
+ return self._create_streaming_response(queryset, field_config, filename,
56
+ headers, localize)
57
+ else:
58
+ return self._create_standard_response(queryset, field_config, filename,
59
+ headers, localize)
60
+
61
+ def serialize_data(self, data, fields=None, filename="export.csv", headers=None):
62
+ """
63
+ Serialize list of dictionaries or objects to CSV.
64
+
65
+ :param data: List of dictionaries or objects
66
+ :param fields: Field names to include
67
+ :param filename: Output filename
68
+ :param headers: Custom header names
69
+ :return: HttpResponse
70
+ """
71
+ if not data:
72
+ return self._create_empty_response(filename)
73
+
74
+ # Auto-detect fields if not provided
75
+ if not fields:
76
+ fields = self._auto_detect_fields(data[0])
77
+
78
+ # Prepare field configuration
79
+ field_config = self._prepare_field_config(fields, headers)
80
+
81
+ # Generate CSV content
82
+ output = io.StringIO()
83
+ writer = csv.writer(output, delimiter=self.delimiter,
84
+ quotechar=self.quotechar, quoting=self.quoting)
85
+
86
+ # Write header
87
+ writer.writerow(field_config['headers'])
88
+
89
+ # Write data rows
90
+ for item in data:
91
+ row = self._extract_row_data(item, field_config['field_names'])
92
+ writer.writerow(row)
93
+
94
+ # Create response
95
+ response = HttpResponse(output.getvalue(), content_type='text/csv')
96
+ response['Content-Disposition'] = f'attachment; filename={filename}'
97
+ return response
98
+
99
+ def _get_field_config(self, queryset, fields, graph):
100
+ """
101
+ Get field configuration from various sources.
102
+ """
103
+ if fields:
104
+ return self._prepare_field_config(fields)
105
+
106
+ # Try to get from RestMeta.GRAPHS
107
+ if graph and hasattr(queryset.model, 'RestMeta'):
108
+ rest_meta = queryset.model.RestMeta
109
+ if hasattr(rest_meta, 'GRAPHS') and graph in rest_meta.GRAPHS:
110
+ graph_config = rest_meta.GRAPHS[graph]
111
+ graph_fields = graph_config.get('fields', [])
112
+ if graph_fields:
113
+ return self._prepare_field_config(graph_fields)
114
+
115
+ # Fallback to model fields
116
+ model_fields = [f.name for f in queryset.model._meta.fields]
117
+ return self._prepare_field_config(model_fields)
118
+
119
+ def _prepare_field_config(self, fields, headers=None):
120
+ """
121
+ Prepare field configuration for CSV generation.
122
+ """
123
+ field_names = []
124
+ field_headers = []
125
+
126
+ for i, field in enumerate(fields):
127
+ if isinstance(field, (tuple, list)):
128
+ field_name, display_name = field
129
+ field_names.append(field_name)
130
+ field_headers.append(display_name)
131
+ else:
132
+ field_names.append(field)
133
+ field_headers.append(field.replace('_', ' ').replace('.', ' ').title())
134
+
135
+ # Override with custom headers if provided
136
+ if headers:
137
+ field_headers = headers[:len(field_names)]
138
+
139
+ return {
140
+ 'field_names': field_names,
141
+ 'headers': field_headers
142
+ }
143
+
144
+ def _create_streaming_response(self, queryset, field_config, filename,
145
+ headers, localize):
146
+ """
147
+ Create streaming HTTP response for large datasets.
148
+ """
149
+ logger.info(f"Creating streaming CSV response for {queryset.count()} records")
150
+
151
+ def csv_generator():
152
+ # Create CSV writer with pseudo-buffer
153
+ pseudo_buffer = PseudoBuffer()
154
+ writer = csv.writer(pseudo_buffer, delimiter=self.delimiter,
155
+ quotechar=self.quotechar, quoting=self.quoting)
156
+
157
+ # Yield header row
158
+ yield writer.writerow(field_config['headers'])
159
+
160
+ # Yield data rows
161
+ for obj in queryset.iterator(): # Use iterator for memory efficiency
162
+ try:
163
+ row = self._extract_row_data(obj, field_config['field_names'], localize)
164
+ yield writer.writerow(row)
165
+ except Exception as e:
166
+ logger.error(f"Error processing row for object {obj.pk}: {e}")
167
+ # Continue with next row instead of failing completely
168
+ continue
169
+
170
+ response = StreamingHttpResponse(csv_generator(), content_type='text/csv')
171
+ response['Content-Disposition'] = f'attachment; filename={filename}'
172
+ response['Cache-Control'] = 'no-cache'
173
+ return response
174
+
175
+ def _create_standard_response(self, queryset, field_config, filename,
176
+ headers, localize):
177
+ """
178
+ Create standard HTTP response for smaller datasets.
179
+ """
180
+ output = io.StringIO()
181
+ writer = csv.writer(output, delimiter=self.delimiter,
182
+ quotechar=self.quotechar, quoting=self.quoting)
183
+
184
+ # Write header
185
+ writer.writerow(field_config['headers'])
186
+
187
+ # Write data rows
188
+ for obj in queryset:
189
+ try:
190
+ row = self._extract_row_data(obj, field_config['field_names'], localize)
191
+ writer.writerow(row)
192
+ except Exception as e:
193
+ logger.error(f"Error processing row for object {obj.pk}: {e}")
194
+ continue
195
+
196
+ response = HttpResponse(output.getvalue(), content_type='text/csv')
197
+ response['Content-Disposition'] = f'attachment; filename={filename}'
198
+ return response
199
+
200
+ def _create_empty_response(self, filename):
201
+ """
202
+ Create response for empty dataset.
203
+ """
204
+ response = HttpResponse('', content_type='text/csv')
205
+ response['Content-Disposition'] = f'attachment; filename={filename}'
206
+ return response
207
+
208
+ def _extract_row_data(self, obj, field_names, localize=None):
209
+ """
210
+ Extract row data from an object based on field names.
211
+ """
212
+ row = []
213
+
214
+ for field_name in field_names:
215
+ try:
216
+ value = self._get_field_value(obj, field_name)
217
+ value = self._process_field_value(value, field_name, localize)
218
+ row.append(self._format_csv_value(value))
219
+ except Exception as e:
220
+ logger.warning(f"Error extracting field '{field_name}': {e}")
221
+ row.append("N/A")
222
+
223
+ return row
224
+
225
+ def _get_field_value(self, obj, field_name):
226
+ """
227
+ Get field value from object, supporting nested field access, foreign keys, and JSONField traversal.
228
+ """
229
+ # Handle nested field access (e.g., "parent.id", "parent.name", "metadata.defaults.role")
230
+ if '.' in field_name:
231
+ return self._get_nested_field_value(obj, field_name)
232
+
233
+ # Standard field access
234
+ if hasattr(obj, field_name):
235
+ value = getattr(obj, field_name)
236
+ return value() if callable(value) else value
237
+
238
+ # Dictionary-style access
239
+ if isinstance(obj, dict):
240
+ return obj.get(field_name, None)
241
+
242
+ return None
243
+
244
+ def _get_nested_field_value(self, obj, field_path):
245
+ """
246
+ Get value from nested field path like "parent.id", "parent.name", "metadata.defaults.role".
247
+ Uses robust field type detection for ForeignKey and JSONField traversal.
248
+ """
249
+ try:
250
+ parts = field_path.split('.')
251
+ current = obj
252
+
253
+ for i, field_part in enumerate(parts):
254
+ if current is None:
255
+ return None
256
+
257
+ # Check if this is a model field using get_model_field
258
+ field = None
259
+ if hasattr(current, 'get_model_field'):
260
+ field = current.get_model_field(field_part)
261
+ elif hasattr(current.__class__, 'get_model_field'):
262
+ field = current.__class__.get_model_field(field_part)
263
+
264
+ if field:
265
+ if field.get_internal_type() == "ForeignKey":
266
+ # Handle foreign key field
267
+ current = getattr(current, field_part, None)
268
+ if current is None:
269
+ return None
270
+ elif field.get_internal_type() == "JSONField":
271
+ # Handle JSONField using jsonfield_as_objict
272
+ if hasattr(current, 'jsonfield_as_objict'):
273
+ json_obj = current.jsonfield_as_objict(field_part)
274
+ # Get remaining path for JSONField traversal
275
+ remaining_path = '.'.join(parts[i+1:])
276
+ if remaining_path:
277
+ return json_obj.get(remaining_path, "N/A")
278
+ else:
279
+ return json_obj
280
+ else:
281
+ # Fallback to direct access
282
+ current = getattr(current, field_part, {})
283
+ if not isinstance(current, dict):
284
+ return current
285
+ else:
286
+ # Regular field
287
+ current = getattr(current, field_part, None)
288
+ else:
289
+ # No model field found, try direct access
290
+ if hasattr(current, field_part):
291
+ current = getattr(current, field_part)
292
+ elif isinstance(current, dict):
293
+ current = current.get(field_part)
294
+ else:
295
+ return None
296
+
297
+ # Handle callable attributes
298
+ if callable(current):
299
+ current = current()
300
+
301
+ return current
302
+ except Exception as e:
303
+ logger.warning(f"Error accessing nested field '{field_path}': {e}")
304
+ return None
305
+
306
+ def _process_field_value(self, value, field_name, localize=None):
307
+ """
308
+ Process field value with localization and special handling.
309
+ """
310
+ if value is None:
311
+ return "N/A"
312
+
313
+ # Apply localization if configured
314
+ if localize and field_name in localize:
315
+ try:
316
+ localizer_config = localize[field_name]
317
+ if '|' in localizer_config:
318
+ localizer_name, extra = localizer_config.split('|', 1)
319
+ else:
320
+ localizer_name, extra = localizer_config, None
321
+
322
+ # Import and apply localizer
323
+ from mojo.serializers.formats.localizers import get_localizer
324
+ localizer = get_localizer(localizer_name)
325
+ if localizer:
326
+ return localizer(value, extra)
327
+ except Exception as e:
328
+ logger.warning(f"Localization failed for field '{field_name}': {e}")
329
+
330
+ return value
331
+
332
+ def _format_csv_value(self, value):
333
+ """
334
+ Format value for CSV output.
335
+ """
336
+ if value is None:
337
+ return ""
338
+
339
+ # Handle model instances
340
+ if hasattr(value, 'pk'):
341
+ return str(value.pk)
342
+
343
+ # Handle datetime objects
344
+ elif isinstance(value, datetime):
345
+ return value.strftime('%Y-%m-%d %H:%M:%S')
346
+ elif isinstance(value, date):
347
+ return value.strftime('%Y-%m-%d')
348
+
349
+ # Handle numeric types
350
+ elif isinstance(value, Decimal):
351
+ return str(float(value)) if not value.is_nan() else "0"
352
+ elif isinstance(value, (int, float)):
353
+ return str(value)
354
+
355
+ # Handle collections
356
+ elif isinstance(value, (list, tuple)):
357
+ return '; '.join(str(item) for item in value)
358
+ elif isinstance(value, dict):
359
+ return str(value) # Could be enhanced with better dict formatting
360
+
361
+ # Default string conversion
362
+ else:
363
+ return str(value)
364
+
365
+ def _auto_detect_fields(self, sample_item):
366
+ """
367
+ Auto-detect fields from a sample data item.
368
+ """
369
+ if isinstance(sample_item, dict):
370
+ return list(sample_item.keys())
371
+ elif hasattr(sample_item, '_meta'):
372
+ return [f.name for f in sample_item._meta.fields]
373
+ elif hasattr(sample_item, '__dict__'):
374
+ return list(sample_item.__dict__.keys())
375
+ else:
376
+ return ['value'] # Fallback for primitive types
377
+
378
+
379
+ class PseudoBuffer:
380
+ """
381
+ A buffer for streaming CSV generation.
382
+ """
383
+
384
+ def writerow(self, row):
385
+ """Write the row by returning it as a CSV line."""
386
+ output = io.StringIO()
387
+ writer = csv.writer(output)
388
+ writer.writerow(row)
389
+ return output.getvalue()
390
+
391
+ def write(self, value):
392
+ """Write the value by returning it directly."""
393
+ return value