simo 2.7.14__py3-none-any.whl → 2.7.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.

Potentially problematic release.


This version of simo might be problematic. Click here for more details.

Files changed (334) hide show
  1. simo/__pycache__/__init__.cpython-312.pyc +0 -0
  2. simo/__pycache__/asgi.cpython-312.pyc +0 -0
  3. simo/__pycache__/celeryc.cpython-312.pyc +0 -0
  4. simo/__pycache__/conf.cpython-312.pyc +0 -0
  5. simo/__pycache__/settings.cpython-312.pyc +0 -0
  6. simo/__pycache__/urls.cpython-312.pyc +0 -0
  7. simo/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  8. simo/automation/__pycache__/app_widgets.cpython-312.pyc +0 -0
  9. simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
  10. simo/automation/__pycache__/forms.cpython-312.pyc +0 -0
  11. simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
  12. simo/automation/__pycache__/helpers.cpython-312.pyc +0 -0
  13. simo/automation/__pycache__/models.cpython-312.pyc +0 -0
  14. simo/automation/__pycache__/serializers.cpython-312.pyc +0 -0
  15. simo/automation/__pycache__/state.cpython-312.pyc +0 -0
  16. simo/automation/gateways.py +6 -5
  17. simo/automation/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  18. simo/automation/migrations/__pycache__/0002_update_helpers_in_scripts.cpython-312.pyc +0 -0
  19. simo/automation/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  20. simo/backups/__pycache__/__init__.cpython-312.pyc +0 -0
  21. simo/backups/__pycache__/admin.cpython-312.pyc +0 -0
  22. simo/backups/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  23. simo/backups/__pycache__/models.cpython-312.pyc +0 -0
  24. simo/backups/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  25. simo/backups/migrations/__pycache__/0002_backuplog_backup_level_backup_size.cpython-312.pyc +0 -0
  26. simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-312.pyc +0 -0
  27. simo/backups/migrations/__pycache__/0004_alter_backup_options_alter_backuplog_options_and_more.cpython-312.pyc +0 -0
  28. simo/backups/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  29. simo/core/__pycache__/__init__.cpython-312.pyc +0 -0
  30. simo/core/__pycache__/admin.cpython-312.pyc +0 -0
  31. simo/core/__pycache__/api.cpython-312.pyc +0 -0
  32. simo/core/__pycache__/api_auth.cpython-312.pyc +0 -0
  33. simo/core/__pycache__/api_meta.cpython-312.pyc +0 -0
  34. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  35. simo/core/__pycache__/app_widgets.cpython-312.pyc +0 -0
  36. simo/core/__pycache__/apps.cpython-312.pyc +0 -0
  37. simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
  38. simo/core/__pycache__/autocomplete_views.cpython-312.pyc +0 -0
  39. simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
  40. simo/core/__pycache__/context.cpython-312.pyc +0 -0
  41. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  42. simo/core/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  43. simo/core/__pycache__/events.cpython-312.pyc +0 -0
  44. simo/core/__pycache__/filters.cpython-312.pyc +0 -0
  45. simo/core/__pycache__/form_fields.cpython-312.pyc +0 -0
  46. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  47. simo/core/__pycache__/forms.cpython-312.pyc +0 -0
  48. simo/core/__pycache__/gateways.cpython-312.pyc +0 -0
  49. simo/core/__pycache__/loggers.cpython-312.pyc +0 -0
  50. simo/core/__pycache__/managers.cpython-312.pyc +0 -0
  51. simo/core/__pycache__/middleware.cpython-312.pyc +0 -0
  52. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  53. simo/core/__pycache__/permissions.cpython-312.pyc +0 -0
  54. simo/core/__pycache__/routing.cpython-312.pyc +0 -0
  55. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  56. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  57. simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
  58. simo/core/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  59. simo/core/__pycache__/storage.cpython-312.pyc +0 -0
  60. simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
  61. simo/core/__pycache__/todos.cpython-312.pyc +0 -0
  62. simo/core/__pycache__/views.cpython-312.pyc +0 -0
  63. simo/core/__pycache__/widgets.cpython-312.pyc +0 -0
  64. simo/core/api_meta.py +2 -1
  65. simo/core/db_backend/__pycache__/__init__.cpython-312.pyc +0 -0
  66. simo/core/db_backend/__pycache__/base.cpython-312.pyc +0 -0
  67. simo/core/drf_braces/__pycache__/__init__.cpython-312.pyc +0 -0
  68. simo/core/drf_braces/__pycache__/utils.cpython-312.pyc +0 -0
  69. simo/core/drf_braces/fields/__pycache__/__init__.cpython-312.pyc +0 -0
  70. simo/core/drf_braces/fields/__pycache__/_fields.cpython-312.pyc +0 -0
  71. simo/core/drf_braces/fields/__pycache__/custom.cpython-312.pyc +0 -0
  72. simo/core/drf_braces/fields/__pycache__/mixins.cpython-312.pyc +0 -0
  73. simo/core/drf_braces/fields/__pycache__/modified.cpython-312.pyc +0 -0
  74. simo/core/drf_braces/serializers/__pycache__/__init__.cpython-312.pyc +0 -0
  75. simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-312.pyc +0 -0
  76. simo/core/form_fields.py +6 -0
  77. simo/core/management/__pycache__/__init__.cpython-312.pyc +0 -0
  78. simo/core/management/commands/__pycache__/__init__.cpython-312.pyc +0 -0
  79. simo/core/management/commands/__pycache__/gateways_manager.cpython-312.pyc +0 -0
  80. simo/core/management/commands/__pycache__/on_http_start.cpython-38.pyc +0 -0
  81. simo/core/management/commands/on_http_start.py +16 -0
  82. simo/core/migrations/0049_alter_gateway_type.py +18 -0
  83. simo/core/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  84. simo/core/migrations/__pycache__/0002_load_icons.cpython-312.pyc +0 -0
  85. simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-312.pyc +0 -0
  86. simo/core/migrations/__pycache__/0004_create_generic.cpython-312.pyc +0 -0
  87. simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-312.pyc +0 -0
  88. simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-312.pyc +0 -0
  89. simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-312.pyc +0 -0
  90. simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-312.pyc +0 -0
  91. simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-312.pyc +0 -0
  92. simo/core/migrations/__pycache__/0010_historyaggregate.cpython-312.pyc +0 -0
  93. simo/core/migrations/__pycache__/0011_component_last_change.cpython-312.pyc +0 -0
  94. simo/core/migrations/__pycache__/0012_instance.cpython-312.pyc +0 -0
  95. simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-312.pyc +0 -0
  96. simo/core/migrations/__pycache__/0014_zone_instance.cpython-312.pyc +0 -0
  97. simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-312.pyc +0 -0
  98. simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-312.pyc +0 -0
  99. simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-312.pyc +0 -0
  100. simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-312.pyc +0 -0
  101. simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-312.pyc +0 -0
  102. simo/core/migrations/__pycache__/0020_component_meta.cpython-312.pyc +0 -0
  103. simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-312.pyc +0 -0
  104. simo/core/migrations/__pycache__/0022_auto_20231221_0735.cpython-312.pyc +0 -0
  105. simo/core/migrations/__pycache__/0023_auto_20231229_1352.cpython-312.pyc +0 -0
  106. simo/core/migrations/__pycache__/0024_alter_instance_device_report_history_days.cpython-312.pyc +0 -0
  107. simo/core/migrations/__pycache__/0025_auto_20240122_1321.cpython-312.pyc +0 -0
  108. simo/core/migrations/__pycache__/0026_category_instance.cpython-312.pyc +0 -0
  109. simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-312.pyc +0 -0
  110. simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-312.pyc +0 -0
  111. simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-312.pyc +0 -0
  112. simo/core/migrations/__pycache__/0030_alter_instance_timezone.cpython-312.pyc +0 -0
  113. simo/core/migrations/__pycache__/0031_auto_20240429_1231.cpython-312.pyc +0 -0
  114. simo/core/migrations/__pycache__/0032_auto_20240506_0834.cpython-312.pyc +0 -0
  115. simo/core/migrations/__pycache__/0033_auto_20240509_0821.cpython-312.pyc +0 -0
  116. simo/core/migrations/__pycache__/0034_component_error_msg.cpython-312.pyc +0 -0
  117. simo/core/migrations/__pycache__/0035_remove_instance_share_location.cpython-312.pyc +0 -0
  118. simo/core/migrations/__pycache__/0036_auto_20240521_0823.cpython-312.pyc +0 -0
  119. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-312.pyc +0 -0
  120. simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-312.pyc +0 -0
  121. simo/core/migrations/__pycache__/0039_instance_is_active_alter_instance_timezone.cpython-312.pyc +0 -0
  122. simo/core/migrations/__pycache__/0040_alter_instance_name.cpython-312.pyc +0 -0
  123. simo/core/migrations/__pycache__/0041_alter_instance_slug.cpython-312.pyc +0 -0
  124. simo/core/migrations/__pycache__/0042_alter_instance_timezone.cpython-312.pyc +0 -0
  125. simo/core/migrations/__pycache__/0043_alter_category_instance_alter_instance_timezone_and_more.cpython-312.pyc +0 -0
  126. simo/core/migrations/__pycache__/0044_alter_gateway_type.cpython-312.pyc +0 -0
  127. simo/core/migrations/__pycache__/0045_alter_instance_device_report_history_days_and_more.cpython-312.pyc +0 -0
  128. simo/core/migrations/__pycache__/0046_component_value_translation_alter_gateway_type.cpython-312.pyc +0 -0
  129. simo/core/migrations/__pycache__/0047_alter_component_value_translation.cpython-312.pyc +0 -0
  130. simo/core/migrations/__pycache__/0048_publicfile_privatefile.cpython-312.pyc +0 -0
  131. simo/core/migrations/__pycache__/0049_alter_gateway_type.cpython-312.pyc +0 -0
  132. simo/core/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  133. simo/core/serializers.py +8 -3
  134. simo/core/templatetags/__pycache__/__init__.cpython-312.pyc +0 -0
  135. simo/core/templatetags/__pycache__/components_list.cpython-312.pyc +0 -0
  136. simo/core/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  137. simo/core/utils/__pycache__/admin.cpython-312.pyc +0 -0
  138. simo/core/utils/__pycache__/api.cpython-312.pyc +0 -0
  139. simo/core/utils/__pycache__/cache.cpython-312.pyc +0 -0
  140. simo/core/utils/__pycache__/config_values.cpython-312.pyc +0 -0
  141. simo/core/utils/__pycache__/converters.cpython-312.pyc +0 -0
  142. simo/core/utils/__pycache__/easing.cpython-312.pyc +0 -0
  143. simo/core/utils/__pycache__/form_fields.cpython-312.pyc +0 -0
  144. simo/core/utils/__pycache__/form_widgets.cpython-312.pyc +0 -0
  145. simo/core/utils/__pycache__/formsets.cpython-312.pyc +0 -0
  146. simo/core/utils/__pycache__/helpers.cpython-312.pyc +0 -0
  147. simo/core/utils/__pycache__/json.cpython-312.pyc +0 -0
  148. simo/core/utils/__pycache__/logs.cpython-312.pyc +0 -0
  149. simo/core/utils/__pycache__/mixins.cpython-312.pyc +0 -0
  150. simo/core/utils/__pycache__/model_helpers.cpython-312.pyc +0 -0
  151. simo/core/utils/__pycache__/operations.cpython-312.pyc +0 -0
  152. simo/core/utils/__pycache__/relay.cpython-312.pyc +0 -0
  153. simo/core/utils/__pycache__/serialization.cpython-312.pyc +0 -0
  154. simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
  155. simo/core/utils/__pycache__/validators.cpython-312.pyc +0 -0
  156. simo/fleet/__pycache__/__init__.cpython-312.pyc +0 -0
  157. simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
  158. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  159. simo/fleet/__pycache__/auto_urls.cpython-312.pyc +0 -0
  160. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  161. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  162. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  163. simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
  164. simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
  165. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  166. simo/fleet/__pycache__/routing.cpython-312.pyc +0 -0
  167. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  168. simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  169. simo/fleet/__pycache__/utils.cpython-312.pyc +0 -0
  170. simo/fleet/__pycache__/views.cpython-312.pyc +0 -0
  171. simo/fleet/controllers.py +8 -0
  172. simo/fleet/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  173. simo/fleet/migrations/__pycache__/0002_auto_20220422_0743.cpython-312.pyc +0 -0
  174. simo/fleet/migrations/__pycache__/0003_auto_20220422_0752.cpython-312.pyc +0 -0
  175. simo/fleet/migrations/__pycache__/0004_auto_20220422_0818.cpython-312.pyc +0 -0
  176. simo/fleet/migrations/__pycache__/0005_auto_20220428_0900.cpython-312.pyc +0 -0
  177. simo/fleet/migrations/__pycache__/0006_rename_mac_colonel_uid.cpython-312.pyc +0 -0
  178. simo/fleet/migrations/__pycache__/0007_colonel_socket_connected.cpython-312.pyc +0 -0
  179. simo/fleet/migrations/__pycache__/0008_i2cinterface.cpython-312.pyc +0 -0
  180. simo/fleet/migrations/__pycache__/0009_i2cinterface_name.cpython-312.pyc +0 -0
  181. simo/fleet/migrations/__pycache__/0010_auto_20220602_0746.cpython-312.pyc +0 -0
  182. simo/fleet/migrations/__pycache__/0011_i2cinterface_freq.cpython-312.pyc +0 -0
  183. simo/fleet/migrations/__pycache__/0012_colonel_logs_stream.cpython-312.pyc +0 -0
  184. simo/fleet/migrations/__pycache__/0013_alter_colonel_last_seen.cpython-312.pyc +0 -0
  185. simo/fleet/migrations/__pycache__/0014_auto_20220614_0659.cpython-312.pyc +0 -0
  186. simo/fleet/migrations/__pycache__/0015_auto_20220614_0754.cpython-312.pyc +0 -0
  187. simo/fleet/migrations/__pycache__/0016_auto_20220704_0840.cpython-312.pyc +0 -0
  188. simo/fleet/migrations/__pycache__/0017_alter_colonel_secret.cpython-312.pyc +0 -0
  189. simo/fleet/migrations/__pycache__/0018_colonel_instance.cpython-312.pyc +0 -0
  190. simo/fleet/migrations/__pycache__/0019_auto_20231006_0749.cpython-312.pyc +0 -0
  191. simo/fleet/migrations/__pycache__/0020_instanceoptions.cpython-312.pyc +0 -0
  192. simo/fleet/migrations/__pycache__/0021_auto_20231006_0819.cpython-312.pyc +0 -0
  193. simo/fleet/migrations/__pycache__/0022_remove_colonel_secret.cpython-312.pyc +0 -0
  194. simo/fleet/migrations/__pycache__/0023_colonel_is_authorized.cpython-312.pyc +0 -0
  195. simo/fleet/migrations/__pycache__/0024_colonel_pwm_frequency.cpython-312.pyc +0 -0
  196. simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-312.pyc +0 -0
  197. simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-312.pyc +0 -0
  198. simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-312.pyc +0 -0
  199. simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-312.pyc +0 -0
  200. simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-312.pyc +0 -0
  201. simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-312.pyc +0 -0
  202. simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-312.pyc +0 -0
  203. simo/fleet/migrations/__pycache__/0032_auto_20240415_0736.cpython-312.pyc +0 -0
  204. simo/fleet/migrations/__pycache__/0033_auto_20240415_0736.cpython-312.pyc +0 -0
  205. simo/fleet/migrations/__pycache__/0034_auto_20240418_0735.cpython-312.pyc +0 -0
  206. simo/fleet/migrations/__pycache__/0035_auto_20240514_0855.cpython-312.pyc +0 -0
  207. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-312.pyc +0 -0
  208. simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-312.pyc +0 -0
  209. simo/fleet/migrations/__pycache__/0038_alter_colonel_type.cpython-312.pyc +0 -0
  210. simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-312.pyc +0 -0
  211. simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-312.pyc +0 -0
  212. simo/fleet/migrations/__pycache__/0041_alter_colonel_instance_and_more.cpython-312.pyc +0 -0
  213. simo/fleet/migrations/__pycache__/0042_auto_20241120_1028.cpython-312.pyc +0 -0
  214. simo/fleet/migrations/__pycache__/0043_auto_20241203_0930.cpython-312.pyc +0 -0
  215. simo/fleet/migrations/__pycache__/0044_auto_20241210_0707.cpython-312.pyc +0 -0
  216. simo/fleet/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  217. simo/generic/__pycache__/__init__.cpython-312.pyc +0 -0
  218. simo/generic/__pycache__/app_widgets.cpython-312.pyc +0 -0
  219. simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
  220. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  221. simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
  222. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  223. simo/generic/__pycache__/models.cpython-312.pyc +0 -0
  224. simo/generic/__pycache__/routing.cpython-312.pyc +0 -0
  225. simo/generic/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  226. simo/generic/controllers.py +10 -0
  227. simo/generic/forms.py +15 -11
  228. simo/generic/gateways.py +43 -43
  229. simo/generic/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  230. simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-312.pyc +0 -0
  231. simo/generic/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  232. simo/multimedia/__pycache__/__init__.cpython-312.pyc +0 -0
  233. simo/multimedia/__pycache__/admin.cpython-312.pyc +0 -0
  234. simo/multimedia/__pycache__/api.cpython-312.pyc +0 -0
  235. simo/multimedia/__pycache__/app_widgets.cpython-312.pyc +0 -0
  236. simo/multimedia/__pycache__/app_widgets.cpython-38.pyc +0 -0
  237. simo/multimedia/__pycache__/auto_urls.cpython-312.pyc +0 -0
  238. simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
  239. simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
  240. simo/multimedia/__pycache__/forms.cpython-312.pyc +0 -0
  241. simo/multimedia/__pycache__/models.cpython-312.pyc +0 -0
  242. simo/multimedia/__pycache__/serializers.cpython-312.pyc +0 -0
  243. simo/multimedia/__pycache__/views.cpython-312.pyc +0 -0
  244. simo/multimedia/admin.py +7 -7
  245. simo/multimedia/app_widgets.py +1 -1
  246. simo/multimedia/auto_urls.py +5 -2
  247. simo/multimedia/controllers.py +17 -12
  248. simo/multimedia/migrations/0006_remove_sound_length_sound_duration.py +22 -0
  249. simo/multimedia/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  250. simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-312.pyc +0 -0
  251. simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-312.pyc +0 -0
  252. simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-312.pyc +0 -0
  253. simo/multimedia/migrations/__pycache__/0005_remove_sound_slug_sound_date_uploaded.cpython-312.pyc +0 -0
  254. simo/multimedia/migrations/__pycache__/0006_remove_sound_length_sound_duration.cpython-312.pyc +0 -0
  255. simo/multimedia/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  256. simo/multimedia/models.py +8 -4
  257. simo/multimedia/views.py +63 -1
  258. simo/notifications/__pycache__/__init__.cpython-312.pyc +0 -0
  259. simo/notifications/__pycache__/admin.cpython-312.pyc +0 -0
  260. simo/notifications/__pycache__/api.cpython-312.pyc +0 -0
  261. simo/notifications/__pycache__/models.cpython-312.pyc +0 -0
  262. simo/notifications/__pycache__/serializers.cpython-312.pyc +0 -0
  263. simo/notifications/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  264. simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-312.pyc +0 -0
  265. simo/notifications/migrations/__pycache__/0003_alter_notification_instance.cpython-312.pyc +0 -0
  266. simo/notifications/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  267. simo/urls.py +6 -2
  268. simo/users/__pycache__/__init__.cpython-312.pyc +0 -0
  269. simo/users/__pycache__/admin.cpython-312.pyc +0 -0
  270. simo/users/__pycache__/api.cpython-312.pyc +0 -0
  271. simo/users/__pycache__/apps.cpython-312.pyc +0 -0
  272. simo/users/__pycache__/auth_backends.cpython-312.pyc +0 -0
  273. simo/users/__pycache__/auto_urls.cpython-312.pyc +0 -0
  274. simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  275. simo/users/__pycache__/managers.cpython-312.pyc +0 -0
  276. simo/users/__pycache__/middleware.cpython-312.pyc +0 -0
  277. simo/users/__pycache__/models.cpython-312.pyc +0 -0
  278. simo/users/__pycache__/permissions.cpython-312.pyc +0 -0
  279. simo/users/__pycache__/serializers.cpython-312.pyc +0 -0
  280. simo/users/__pycache__/sso_urls.cpython-312.pyc +0 -0
  281. simo/users/__pycache__/sso_views.cpython-312.pyc +0 -0
  282. simo/users/__pycache__/utils.cpython-312.pyc +0 -0
  283. simo/users/__pycache__/views.cpython-312.pyc +0 -0
  284. simo/users/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  285. simo/users/migrations/__pycache__/0002_componentpermission.cpython-312.pyc +0 -0
  286. simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-312.pyc +0 -0
  287. simo/users/migrations/__pycache__/0004_user_secret_key.cpython-312.pyc +0 -0
  288. simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-312.pyc +0 -0
  289. simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-312.pyc +0 -0
  290. simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-312.pyc +0 -0
  291. simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-312.pyc +0 -0
  292. simo/users/migrations/__pycache__/0009_remove_user_role.cpython-312.pyc +0 -0
  293. simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-312.pyc +0 -0
  294. simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-312.pyc +0 -0
  295. simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-312.pyc +0 -0
  296. simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-312.pyc +0 -0
  297. simo/users/migrations/__pycache__/0014_user_roles.cpython-312.pyc +0 -0
  298. simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-312.pyc +0 -0
  299. simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-312.pyc +0 -0
  300. simo/users/migrations/__pycache__/0017_auto_20231221_0735.cpython-312.pyc +0 -0
  301. simo/users/migrations/__pycache__/0018_user_is_god.cpython-312.pyc +0 -0
  302. simo/users/migrations/__pycache__/0019_auto_20231221_1155.cpython-312.pyc +0 -0
  303. simo/users/migrations/__pycache__/0020_rename_is_god_user_is_master.cpython-312.pyc +0 -0
  304. simo/users/migrations/__pycache__/0021_alter_permissionsrole_instance.cpython-312.pyc +0 -0
  305. simo/users/migrations/__pycache__/0022_userdevicereportlog_instance.cpython-312.pyc +0 -0
  306. simo/users/migrations/__pycache__/0023_auto_20240105_0719.cpython-312.pyc +0 -0
  307. simo/users/migrations/__pycache__/0024_fingerprint.cpython-312.pyc +0 -0
  308. simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-312.pyc +0 -0
  309. simo/users/migrations/__pycache__/0026_fingerprint_name.cpython-312.pyc +0 -0
  310. simo/users/migrations/__pycache__/0027_permissionsrole_can_manage_components.cpython-312.pyc +0 -0
  311. simo/users/migrations/__pycache__/0028_auto_20240506_1146.cpython-312.pyc +0 -0
  312. simo/users/migrations/__pycache__/0029_alter_instanceuser_instance.cpython-312.pyc +0 -0
  313. simo/users/migrations/__pycache__/0030_userdevice_users.cpython-312.pyc +0 -0
  314. simo/users/migrations/__pycache__/0031_auto_20240923_1115.cpython-312.pyc +0 -0
  315. simo/users/migrations/__pycache__/0032_remove_userdevice_user_alter_userdevice_users.cpython-312.pyc +0 -0
  316. simo/users/migrations/__pycache__/0033_alter_user_ssh_key.cpython-312.pyc +0 -0
  317. simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-312.pyc +0 -0
  318. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-312.pyc +0 -0
  319. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-312.pyc +0 -0
  320. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-312.pyc +0 -0
  321. simo/users/migrations/__pycache__/0038_userdevicereportlog_at_home_and_more.cpython-312.pyc +0 -0
  322. simo/users/migrations/__pycache__/0039_auto_20241117_1039.cpython-312.pyc +0 -0
  323. simo/users/migrations/__pycache__/0040_userdevicereportlog_location_smoothed_and_more.cpython-312.pyc +0 -0
  324. simo/users/migrations/__pycache__/0041_userdevicereportlog_speed_kmh_received.cpython-312.pyc +0 -0
  325. simo/users/migrations/__pycache__/0042_remove_userdevicereportlog_location_smoothed_and_more.cpython-312.pyc +0 -0
  326. simo/users/migrations/__pycache__/0043_userdevicereportlog_avg_speed_kmh.cpython-312.pyc +0 -0
  327. simo/users/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  328. simo-2.7.16.dist-info/METADATA +56 -0
  329. {simo-2.7.14.dist-info → simo-2.7.16.dist-info}/RECORD +333 -27
  330. {simo-2.7.14.dist-info → simo-2.7.16.dist-info}/WHEEL +1 -1
  331. simo-2.7.14.dist-info/METADATA +0 -54
  332. {simo-2.7.14.dist-info → simo-2.7.16.dist-info}/LICENSE.md +0 -0
  333. {simo-2.7.14.dist-info → simo-2.7.16.dist-info}/entry_points.txt +0 -0
  334. {simo-2.7.14.dist-info → simo-2.7.16.dist-info}/top_level.txt +0 -0
simo/fleet/controllers.py CHANGED
@@ -72,6 +72,14 @@ class FleeDeviceMixin:
72
72
  config[key] = val
73
73
  return config
74
74
 
75
+ def _call_cmd(self, method, *args, **kwargs):
76
+ GatewayObjectCommand(
77
+ self.component.gateway,
78
+ Colonel(id=self.component.config['colonel']),
79
+ id=self.component.id, command='call', method=method,
80
+ args=args, kwargs=kwargs
81
+ ).publish()
82
+
75
83
 
76
84
  class BasicSensorMixin:
77
85
  gateway_class = FleetGatewayHandler
@@ -1059,6 +1059,16 @@ class AudioAlert(Switch):
1059
1059
  name = _("Audio Alert")
1060
1060
  config_form = AudioAlertConfigForm
1061
1061
 
1062
+ def send(self, value):
1063
+ for player in Component.objects.filter(
1064
+ id__in=self.component.config['players']
1065
+ ):
1066
+ if value:
1067
+ player.play_alert(self.component.id)
1068
+ else:
1069
+ self.component.set(False)
1070
+ player.cancel_alert()
1071
+
1062
1072
 
1063
1073
  class StateSelect(ControllerBase):
1064
1074
  gateway_class = GenericGatewayHandler
simo/generic/forms.py CHANGED
@@ -7,7 +7,7 @@ from django.db.models import Q
7
7
  from django.utils.translation import gettext_lazy as _
8
8
  from django.core.files.uploadedfile import InMemoryUploadedFile
9
9
  from simo.core.forms import HiddenField, BaseComponentForm
10
- from simo.core.models import Icon, Component, PublicFile
10
+ from simo.core.models import Component
11
11
  from simo.core.controllers import (
12
12
  NumericSensor, MultiSensor, Switch, Dimmer
13
13
  )
@@ -22,6 +22,7 @@ from simo.core.form_fields import (
22
22
  )
23
23
  from simo.core.forms import DimmerConfigForm, SwitchForm
24
24
  from simo.core.form_fields import SoundField
25
+ from simo.multimedia.models import Sound
25
26
 
26
27
  ACTION_METHODS = (
27
28
  ('turn_on', "Turn ON"), ('turn_off', "Turn OFF"),
@@ -625,8 +626,9 @@ class AudioAlertConfigForm(BaseComponentForm):
625
626
  loop = forms.BooleanField(initial=False, required=False)
626
627
  volume = forms.IntegerField(initial=30, min_value=2, max_value=100)
627
628
  players = Select2ModelMultipleChoiceField(
628
- queryset=Component.objects.all(),#filter(base_type='audio-player'),
629
+ queryset=Component.objects.filter(base_type='audio-player'),
629
630
  url='autocomplete-component',
631
+ forward=(forward.Const(['audio-player',], 'base_type'),)
630
632
  )
631
633
 
632
634
  def __init__(self, *args, **kwargs):
@@ -669,24 +671,26 @@ class AudioAlertConfigForm(BaseComponentForm):
669
671
 
670
672
  return self.cleaned_data['sound']
671
673
 
672
-
673
674
  def save(self, commit=True):
674
675
  obj = super().save(commit=commit)
675
676
  if type(self.cleaned_data['sound']) != InMemoryUploadedFile:
676
677
  return obj
677
678
 
678
- public_file = PublicFile(component=obj)
679
- public_file.file.save(
679
+ sound = Sound(
680
+ name=self.cleaned_data['sound'].name,
681
+ duration=self.cleaned_data['sound'].duration
682
+ )
683
+ sound.file.save(
680
684
  self.cleaned_data['sound'].name, self.cleaned_data['sound'],
681
685
  save=True
682
686
  )
683
- org = PublicFile.objects.filter(
684
- id=self.instance.config.get('public_file_id', 0)
685
- )
686
- if org:
687
- org.delete()
688
- self.instance.config['public_file_id'] = public_file.id
687
+ Sound.objects.filter(
688
+ id=self.instance.config.get('sound_id', 0)
689
+ ).delete()
690
+ self.instance.config['sound_id'] = sound.id
689
691
  self.instance.config['duration'] = self.cleaned_data['sound'].duration
692
+ self.instance.config['stream_url'] = sound.stream_url()
693
+ self.instance.config['file_url'] = sound.get_absolute_url()
690
694
  self.instance.config['sound'] = self.cleaned_data['sound'].name
691
695
  self.instance.save()
692
696
  return obj
simo/generic/gateways.py CHANGED
@@ -130,49 +130,49 @@ class GroupButtonsHandler:
130
130
  group.toggle()
131
131
 
132
132
 
133
- class AudioAlertsHandler:
134
-
135
- def control_audio_alert(self, component, val):
136
- if val:
137
- public_file = PublicFile.objects.filter(
138
- component=component
139
- ).first()
140
- if not public_file:
141
- return
142
- uri = f"http://{get_self_ip()}{public_file.get_absolute_url()}"
143
- loop = component.config.get('loop', False)
144
- for pl_id in component.config.get('players', []):
145
- player = Component.objects.filter(
146
- id=pl_id, base_type='audio-player'
147
- ).first()
148
- if not player:
149
- continue
150
- player.play_alert(
151
- uri,
152
- component.config.get('loop', False),
153
- component.config.get('volume', 50)
154
- )
155
- if not loop:
156
- def set_done(comp):
157
- comp.set(False)
158
- threading.Timer(
159
- component.config.get('duration', 1),
160
- set_done, args=[component]
161
- )
162
- component.set(True)
163
- else:
164
- for pl_id in component.config.get('players', []):
165
- player = Component.objects.filter(
166
- id=pl_id, base_type='audio-player'
167
- ).first()
168
- if not player:
169
- continue
170
- player.cancel_alert()
171
- component.set(False)
133
+ # class AudioAlertsHandler:
134
+ #
135
+ # def control_audio_alert(self, component, val):
136
+ # if val:
137
+ # public_file = PublicFile.objects.filter(
138
+ # component=component
139
+ # ).first()
140
+ # if not public_file:
141
+ # return
142
+ # uri = f"http://{get_self_ip()}{public_file.get_absolute_url()}"
143
+ # loop = component.config.get('loop', False)
144
+ # for pl_id in component.config.get('players', []):
145
+ # player = Component.objects.filter(
146
+ # id=pl_id, base_type='audio-player'
147
+ # ).first()
148
+ # if not player:
149
+ # continue
150
+ # player.play_alert(
151
+ # uri,
152
+ # component.config.get('loop', False),
153
+ # component.config.get('volume', 50)
154
+ # )
155
+ # if not loop:
156
+ # def set_done(comp):
157
+ # comp.set(False)
158
+ # threading.Timer(
159
+ # component.config.get('duration', 1),
160
+ # set_done, args=[component]
161
+ # )
162
+ # component.set(True)
163
+ # else:
164
+ # for pl_id in component.config.get('players', []):
165
+ # player = Component.objects.filter(
166
+ # id=pl_id, base_type='audio-player'
167
+ # ).first()
168
+ # if not player:
169
+ # continue
170
+ # player.cancel_alert()
171
+ # component.set(False)
172
172
 
173
173
 
174
174
  class GenericGatewayHandler(
175
- BaseObjectCommandsGatewayHandler, GroupButtonsHandler, AudioAlertsHandler
175
+ BaseObjectCommandsGatewayHandler, GroupButtonsHandler
176
176
  ):
177
177
  name = "Generic"
178
178
  config_form = BaseGatewayForm
@@ -268,7 +268,7 @@ class GenericGatewayHandler(
268
268
 
269
269
  def on_mqtt_message(self, client, userdata, msg):
270
270
  print("Mqtt message: ", msg.payload)
271
- from simo.generic.controllers import AlarmGroup, AudioAlert
271
+ from simo.generic.controllers import AlarmGroup#, #AudioAlert
272
272
 
273
273
  payload = json.loads(msg.payload)
274
274
  drop_current_instance()
@@ -278,8 +278,8 @@ class GenericGatewayHandler(
278
278
  try:
279
279
  if component.controller_uid == AlarmGroup.uid:
280
280
  self.control_alarm_group(component, payload.get('set_val'))
281
- elif component.controller_uid == AudioAlert.uid:
282
- self.control_audio_alert(component, payload.get('set_val'))
281
+ # elif component.controller_uid == AudioAlert.uid:
282
+ # self.control_audio_alert(component, payload.get('set_val'))
283
283
  else:
284
284
  component.controller.set(payload.get('set_val'))
285
285
  except Exception:
simo/multimedia/admin.py CHANGED
@@ -7,23 +7,23 @@ from .forms import SoundModelForm
7
7
 
8
8
  @admin.register(Sound)
9
9
  class SoundAdmin(admin.ModelAdmin):
10
- list_display = 'id', 'name', 'file', 'length_display', 'date_uploaded'
10
+ list_display = 'id', 'name', 'file', 'duration_display', 'date_uploaded'
11
11
  search_fields = 'name', 'file'
12
12
  list_display_links = 'id', 'name',
13
13
  form = SoundModelForm
14
- readonly_fields = 'length_display',
14
+ readonly_fields = 'duration_display',
15
15
 
16
- def length_display(self, obj=None):
17
- if obj and obj.length != None:
18
- return str(timedelta(seconds=obj.length))
16
+ def duration_display(self, obj=None):
17
+ if obj and obj.duration != None:
18
+ return str(timedelta(seconds=obj.duration))
19
19
 
20
- length_display.short_description = 'length'
20
+ duration_display.short_description = 'duration'
21
21
 
22
22
  def save_model(self, request, obj, form, change):
23
23
  super().save_model(request, obj, form, change)
24
24
  # need to keep it here as using admin interface skips post_save signals
25
25
  try:
26
- obj.length = int(
26
+ obj.duration = int(
27
27
  librosa.core.get_duration(
28
28
  sr=22050, filename=obj.file.path
29
29
  )
@@ -5,7 +5,7 @@ from simo.core.app_widgets import BaseAppWidget
5
5
  class AudioPlayerWidget(BaseAppWidget):
6
6
  uid = 'audio-player'
7
7
  name = _('Audio Player')
8
- size = [2, 2]
8
+ size = [4, 1]
9
9
 
10
10
 
11
11
  class VideoPlayerWidget(BaseAppWidget):
@@ -1,10 +1,13 @@
1
1
  from django.urls import path, re_path
2
- from .views import SoundAutocomplete
2
+ from .views import SoundAutocomplete, sound_stream
3
3
 
4
4
 
5
5
  urlpatterns = [
6
6
  path(
7
- 'autocomplete-sound',
7
+ 'autocomplete-sound/',
8
8
  SoundAutocomplete.as_view(), name='autocomplete-sound'
9
+ ),
10
+ path(
11
+ 'sound-<int:sound_id>-stream/', sound_stream, name='sound-stream'
9
12
  )
10
13
  ]
@@ -17,6 +17,7 @@ class BasePlayer(Switch):
17
17
  'duration': None,
18
18
  'position': None,
19
19
  'title': None,
20
+ 'image_url': None,
20
21
  'library': []
21
22
  }
22
23
  default_value = 'stopped'
@@ -89,18 +90,22 @@ class BasePlayer(Switch):
89
90
  assert 0 <= volume <= 100
90
91
  self.send({"play_uri": uri, 'volume': volume})
91
92
 
92
- def play_alert(self, val, loop=False, volume=None):
93
- '''
94
- Plays alert and goes back to whatever was playing initially
95
- :param val: uri
96
- :param loop: Repeat infinitely
97
- :param volume: volume at which to play
98
- :return:
99
- '''
100
- assert type(val) == str
101
- if volume:
102
- assert 0 <= volume <= 100
103
- self.send({"alert": val, 'loop': loop, 'volume': volume})
93
+ # def play_alert(self, val, loop=False, volume=None):
94
+ # '''
95
+ # Plays alert and goes back to whatever was playing initially
96
+ # :param val: uri
97
+ # :param loop: Repeat infinitely
98
+ # :param volume: volume at which to play
99
+ # :return:
100
+ # '''
101
+ # assert type(val) == str
102
+ # if volume:
103
+ # assert 0 <= volume <= 100
104
+ # self.send({"alert": val, 'loop': loop, 'volume': volume})
105
+
106
+
107
+ def play_alert(self, id):
108
+ self.send({"alert": id})
104
109
 
105
110
  def cancel_alert(self):
106
111
  '''Cancel alert if it's currently playing'''
@@ -0,0 +1,22 @@
1
+ # Generated by Django 4.2.10 on 2024-12-22 07:42
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('multimedia', '0005_remove_sound_slug_sound_date_uploaded'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='sound',
15
+ name='length',
16
+ ),
17
+ migrations.AddField(
18
+ model_name='sound',
19
+ name='duration',
20
+ field=models.PositiveIntegerField(default=0, editable=False, help_text='Sound duration in seconds'),
21
+ ),
22
+ ]
simo/multimedia/models.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os, librosa
2
+ from django.urls import reverse
2
3
  from django.db import models
3
4
  from django.db.models.signals import post_save, post_delete
4
5
  from django.dispatch import receiver
@@ -15,8 +16,8 @@ class Sound(models.Model):
15
16
  )
16
17
  )
17
18
  note = models.TextField(null=True, blank=True)
18
- length = models.PositiveIntegerField(
19
- editable=False, default=0, help_text='Sound length in seconds'
19
+ duration = models.PositiveIntegerField(
20
+ editable=False, default=0, help_text='Sound duration in seconds'
20
21
  )
21
22
  date_uploaded = models.DateTimeField(auto_now_add=True)
22
23
 
@@ -26,11 +27,14 @@ class Sound(models.Model):
26
27
  def get_absolute_url(self):
27
28
  return self.file.url
28
29
 
30
+ def stream_url(self):
31
+ return reverse('sound-stream', kwargs={'sound_id': self.id})
32
+
29
33
 
30
34
  @receiver(post_save, sender=Sound)
31
35
  def determine_duration(sender, instance, created, **kwargs):
32
- if not instance.length:
33
- instance.length = int(
36
+ if not instance.duration:
37
+ instance.duration = int(
34
38
  librosa.core.get_duration(
35
39
  sr=22050, filename=instance.file.path
36
40
  )
simo/multimedia/views.py CHANGED
@@ -1,4 +1,9 @@
1
+ import re, os
1
2
  from django.http import Http404
3
+ from django.conf import settings
4
+ from wsgiref.util import FileWrapper
5
+ from django.shortcuts import get_object_or_404
6
+ from django.http import StreamingHttpResponse
2
7
  from dal import autocomplete
3
8
  from simo.core.utils.helpers import search_queryset
4
9
  from .models import Sound
@@ -16,4 +21,61 @@ class SoundAutocomplete(autocomplete.Select2QuerySetView):
16
21
  qs = qs.filter(pk__in=self.request.GET['value'].split(','))
17
22
  elif self.q:
18
23
  qs = search_queryset(qs, self.q, ('name', 'slug'))
19
- return qs
24
+ return qs
25
+
26
+
27
+
28
+
29
+
30
+ def file_iterator(file_path, chunk_size=8192, offset=0, length=None):
31
+ with open(file_path, "rb") as f:
32
+ f.seek(offset, os.SEEK_SET)
33
+ remaining = length
34
+ while True:
35
+ bytes_length = (
36
+ chunk_size
37
+ if remaining is None
38
+ else min(remaining, chunk_size)
39
+ )
40
+ data = f.read(bytes_length)
41
+ if not data:
42
+ break
43
+ if remaining:
44
+ remaining -= len(data)
45
+ yield data
46
+
47
+
48
+
49
+ def sound_stream(request, sound_id):
50
+ sound = get_object_or_404(Sound, id=sound_id)
51
+ path = sound.file.path
52
+ content_type = "audio/" + path.split('.')[-1]
53
+
54
+ range_header = request.META.get("HTTP_RANGE", "").strip()
55
+ RANGE_RE = re.compile(r"bytes\s*=\s*(\d+)\s*-\s*(\d*)", re.I)
56
+ range_match = RANGE_RE.match(range_header)
57
+ size = os.path.getsize(path)
58
+
59
+ if range_match:
60
+ print(f"RANGE HEADER: {range_header}")
61
+ first_byte, last_byte = range_match.groups()
62
+ first_byte = int(first_byte) if first_byte else 0
63
+ last_byte = (
64
+ first_byte + 1024 * 1024 * 8
65
+ ) # The max volume of the response body is 8M per piece
66
+ if last_byte >= size:
67
+ last_byte = size - 1
68
+ length = last_byte - first_byte + 1
69
+ response = StreamingHttpResponse(
70
+ file_iterator(path, offset=first_byte, length=length),
71
+ status=206,
72
+ content_type=content_type,
73
+ )
74
+ response["Content-Range"] = f"bytes {first_byte}-{last_byte}/{size}"
75
+
76
+ else:
77
+ response = StreamingHttpResponse(
78
+ FileWrapper(open(path, "rb")), content_type=content_type
79
+ )
80
+ response["Accept-Ranges"] = "bytes"
81
+ return response