simo 1.7.20__py3-none-any.whl → 2.0.0__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 (267) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/settings.cpython-38.pyc +0 -0
  3. simo/__pycache__/urls.cpython-38.pyc +0 -0
  4. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/autocomplete_views.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/context.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/events.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/managers.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  21. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  22. simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  23. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  24. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  25. simo/core/admin.py +28 -18
  26. simo/core/api.py +157 -16
  27. simo/core/api_meta.py +87 -0
  28. simo/core/auto_urls.py +4 -1
  29. simo/core/autocomplete_views.py +8 -4
  30. simo/core/base_types.py +1 -0
  31. simo/core/context.py +3 -1
  32. simo/core/controllers.py +112 -32
  33. simo/core/db_backend/base.py +7 -22
  34. simo/core/drf_braces/README +3 -0
  35. simo/core/drf_braces/__init__.py +7 -0
  36. simo/core/drf_braces/__pycache__/__init__.cpython-38.pyc +0 -0
  37. simo/core/drf_braces/__pycache__/utils.cpython-38.pyc +0 -0
  38. simo/core/drf_braces/fields/__init__.py +5 -0
  39. simo/core/drf_braces/fields/__pycache__/__init__.cpython-38.pyc +0 -0
  40. simo/core/drf_braces/fields/__pycache__/_fields.cpython-38.pyc +0 -0
  41. simo/core/drf_braces/fields/__pycache__/custom.cpython-38.pyc +0 -0
  42. simo/core/drf_braces/fields/__pycache__/mixins.cpython-38.pyc +0 -0
  43. simo/core/drf_braces/fields/__pycache__/modified.cpython-38.pyc +0 -0
  44. simo/core/drf_braces/fields/_fields.py +48 -0
  45. simo/core/drf_braces/fields/custom.py +107 -0
  46. simo/core/drf_braces/fields/mixins.py +58 -0
  47. simo/core/drf_braces/fields/modified.py +41 -0
  48. simo/core/drf_braces/forms/__init__.py +0 -0
  49. simo/core/drf_braces/forms/fields.py +20 -0
  50. simo/core/drf_braces/forms/serializer_form.py +156 -0
  51. simo/core/drf_braces/mixins.py +52 -0
  52. simo/core/drf_braces/models.py +0 -0
  53. simo/core/drf_braces/parsers.py +72 -0
  54. simo/core/drf_braces/renderers.py +37 -0
  55. simo/core/drf_braces/serializers/__init__.py +0 -0
  56. simo/core/drf_braces/serializers/__pycache__/__init__.cpython-38.pyc +0 -0
  57. simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc +0 -0
  58. simo/core/drf_braces/serializers/enforce_validation_serializer.py +214 -0
  59. simo/core/drf_braces/serializers/form_serializer.py +391 -0
  60. simo/core/drf_braces/serializers/swapping.py +48 -0
  61. simo/core/drf_braces/tests/__init__.py +0 -0
  62. simo/core/drf_braces/tests/fields/__init__.py +0 -0
  63. simo/core/drf_braces/tests/fields/test_custom.py +94 -0
  64. simo/core/drf_braces/tests/fields/test_fields.py +13 -0
  65. simo/core/drf_braces/tests/fields/test_mixins.py +96 -0
  66. simo/core/drf_braces/tests/fields/test_modified.py +40 -0
  67. simo/core/drf_braces/tests/forms/__init__.py +0 -0
  68. simo/core/drf_braces/tests/forms/test_fields.py +46 -0
  69. simo/core/drf_braces/tests/forms/test_serializer_form.py +256 -0
  70. simo/core/drf_braces/tests/serializers/__init__.py +0 -0
  71. simo/core/drf_braces/tests/serializers/test_enforce_validation_serializer.py +169 -0
  72. simo/core/drf_braces/tests/serializers/test_form_serializer.py +387 -0
  73. simo/core/drf_braces/tests/serializers/test_swapping.py +40 -0
  74. simo/core/drf_braces/tests/test_mixins.py +111 -0
  75. simo/core/drf_braces/tests/test_parsers.py +73 -0
  76. simo/core/drf_braces/tests/test_renderers.py +23 -0
  77. simo/core/drf_braces/tests/test_utils.py +73 -0
  78. simo/core/drf_braces/utils.py +209 -0
  79. simo/core/events.py +3 -3
  80. simo/core/forms.py +79 -37
  81. simo/core/gateways.py +31 -14
  82. simo/core/management/commands/gateways_manager.py +0 -1
  83. simo/core/managers.py +81 -0
  84. simo/core/middleware.py +25 -0
  85. simo/core/migrations/0026_category_instance.py +20 -0
  86. simo/core/migrations/0027_remove_component_tags.py +17 -0
  87. simo/core/migrations/0028_rename_subcomponents_component_slaves.py +18 -0
  88. simo/core/migrations/0029_auto_20240229_1331.py +33 -0
  89. simo/core/migrations/__pycache__/0026_category_instance.cpython-38.pyc +0 -0
  90. simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-38.pyc +0 -0
  91. simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-38.pyc +0 -0
  92. simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-38.pyc +0 -0
  93. simo/core/models.py +103 -66
  94. simo/core/permissions.py +28 -2
  95. simo/core/serializers.py +330 -26
  96. simo/core/socket_consumers.py +5 -14
  97. simo/core/tasks.py +11 -1
  98. simo/core/templates/admin/base.html +37 -10
  99. simo/core/templates/admin/wizard/discovery.html +188 -0
  100. simo/core/templates/admin/wizard/wizard_add.html +5 -5
  101. simo/core/utils/__pycache__/serialization.cpython-38.pyc +0 -0
  102. simo/core/utils/admin.py +9 -2
  103. simo/core/utils/formsets.py +17 -16
  104. simo/core/utils/helpers.py +1 -0
  105. simo/core/utils/serialization.py +56 -0
  106. simo/core/utils/type_constants.py +1 -1
  107. simo/core/utils/validators.py +14 -1
  108. simo/core/views.py +13 -0
  109. simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
  110. simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
  111. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  112. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  113. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  114. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  115. simo/fleet/__pycache__/managers.cpython-38.pyc +0 -0
  116. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  117. simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
  118. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  119. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  120. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  121. simo/fleet/admin.py +54 -25
  122. simo/fleet/api.py +59 -3
  123. simo/fleet/auto_urls.py +2 -3
  124. simo/fleet/controllers.py +199 -16
  125. simo/fleet/forms.py +325 -483
  126. simo/fleet/gateways.py +44 -2
  127. simo/fleet/managers.py +32 -0
  128. simo/fleet/migrations/0025_auto_20240130_1334.py +27 -0
  129. simo/fleet/migrations/0026_rename_i2cinterface_scl_pin_and_more.py +64 -0
  130. simo/fleet/migrations/0027_auto_20240306_0802.py +170 -0
  131. simo/fleet/migrations/0028_remove_i2cinterface_scl_pin_no_and_more.py +21 -0
  132. simo/fleet/migrations/0029_alter_i2cinterface_scl_pin_and_more.py +24 -0
  133. simo/fleet/migrations/0030_colonelpin_label_alter_colonel_type.py +24 -0
  134. simo/fleet/migrations/0031_alter_colonel_type.py +18 -0
  135. simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-38.pyc +0 -0
  136. simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
  137. simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-38.pyc +0 -0
  138. simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-38.pyc +0 -0
  139. simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
  140. simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-38.pyc +0 -0
  141. simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-38.pyc +0 -0
  142. simo/fleet/models.py +134 -82
  143. simo/fleet/serializers.py +35 -1
  144. simo/fleet/socket_consumers.py +239 -76
  145. simo/fleet/utils.py +15 -53
  146. simo/fleet/views.py +28 -14
  147. simo/generic/controllers.py +13 -89
  148. simo/generic/forms.py +29 -18
  149. simo/generic/gateways.py +73 -2
  150. simo/generic/models.py +3 -3
  151. simo/multimedia/controllers.py +9 -8
  152. simo/settings.py +7 -4
  153. simo/urls.py +4 -8
  154. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  155. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  156. simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
  157. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  158. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  159. simo/users/__pycache__/sso_urls.cpython-38.pyc +0 -0
  160. simo/users/admin.py +8 -1
  161. simo/users/api.py +38 -2
  162. simo/users/auto_urls.py +2 -2
  163. simo/users/migrations/0025_rename_name_fingerprint_type_and_more.py +22 -0
  164. simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-38.pyc +0 -0
  165. simo/users/models.py +2 -3
  166. simo/users/serializers.py +15 -1
  167. simo/users/sso_urls.py +3 -3
  168. simo/wsgi.py +7 -0
  169. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/METADATA +8 -9
  170. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/RECORD +173 -189
  171. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/WHEEL +1 -1
  172. simo/core/db_backend/__pycache__/__init__.cpython-38.pyc +0 -0
  173. simo/core/db_backend/__pycache__/base.cpython-38.pyc +0 -0
  174. simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
  175. simo/core/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  176. simo/core/migrations/__pycache__/0002_load_icons.cpython-38.pyc +0 -0
  177. simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
  178. simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
  179. simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-38.pyc +0 -0
  180. simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-38.pyc +0 -0
  181. simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-38.pyc +0 -0
  182. simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-38.pyc +0 -0
  183. simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-38.pyc +0 -0
  184. simo/core/migrations/__pycache__/0010_historyaggregate.cpython-38.pyc +0 -0
  185. simo/core/migrations/__pycache__/0011_component_last_change.cpython-38.pyc +0 -0
  186. simo/core/migrations/__pycache__/0012_instance.cpython-38.pyc +0 -0
  187. simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
  188. simo/core/migrations/__pycache__/0014_zone_instance.cpython-38.pyc +0 -0
  189. simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-38.pyc +0 -0
  190. simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-38.pyc +0 -0
  191. simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-38.pyc +0 -0
  192. simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
  193. simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-38.pyc +0 -0
  194. simo/core/migrations/__pycache__/0020_component_meta.cpython-38.pyc +0 -0
  195. simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-38.pyc +0 -0
  196. simo/core/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  197. simo/core/templatetags/__pycache__/__init__.cpython-38.pyc +0 -0
  198. simo/core/templatetags/__pycache__/components_list.cpython-38.pyc +0 -0
  199. simo/core/utils/__pycache__/__init__.cpython-38.pyc +0 -0
  200. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  201. simo/core/utils/__pycache__/config_values.cpython-38.pyc +0 -0
  202. simo/core/utils/__pycache__/easing.cpython-38.pyc +0 -0
  203. simo/core/utils/__pycache__/form_fields.cpython-38.pyc +0 -0
  204. simo/core/utils/__pycache__/form_widgets.cpython-38.pyc +0 -0
  205. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  206. simo/core/utils/__pycache__/helpers.cpython-38.pyc +0 -0
  207. simo/core/utils/__pycache__/logs.cpython-38.pyc +0 -0
  208. simo/core/utils/__pycache__/mixins.cpython-38.pyc +0 -0
  209. simo/core/utils/__pycache__/model_helpers.cpython-38.pyc +0 -0
  210. simo/core/utils/__pycache__/relay.cpython-38.pyc +0 -0
  211. simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
  212. simo/core/utils/__pycache__/validators.cpython-38.pyc +0 -0
  213. simo/fleet/tasks.py +0 -25
  214. simo/generic/__pycache__/__init__.cpython-37.pyc +0 -0
  215. simo/generic/__pycache__/__init__.cpython-38.pyc +0 -0
  216. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  217. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  218. simo/generic/__pycache__/controllers.cpython-37.pyc +0 -0
  219. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  220. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  221. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  222. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  223. simo/generic/__pycache__/routing.cpython-38.pyc +0 -0
  224. simo/generic/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  225. simo/generic/__pycache__/widgets.cpython-37.pyc +0 -0
  226. simo/multimedia/__pycache__/__init__.cpython-38.pyc +0 -0
  227. simo/multimedia/__pycache__/admin.cpython-38.pyc +0 -0
  228. simo/multimedia/__pycache__/api.cpython-38.pyc +0 -0
  229. simo/multimedia/__pycache__/app_widgets.cpython-38.pyc +0 -0
  230. simo/multimedia/__pycache__/base_types.cpython-38.pyc +0 -0
  231. simo/multimedia/__pycache__/controllers.cpython-38.pyc +0 -0
  232. simo/multimedia/__pycache__/forms.cpython-38.pyc +0 -0
  233. simo/multimedia/__pycache__/models.cpython-38.pyc +0 -0
  234. simo/multimedia/__pycache__/serializers.cpython-38.pyc +0 -0
  235. simo/multimedia/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  236. simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-38.pyc +0 -0
  237. simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-38.pyc +0 -0
  238. simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-38.pyc +0 -0
  239. simo/multimedia/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  240. simo/notifications/__pycache__/__init__.cpython-38.pyc +0 -0
  241. simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
  242. simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
  243. simo/notifications/__pycache__/models.cpython-38.pyc +0 -0
  244. simo/notifications/__pycache__/serializers.cpython-38.pyc +0 -0
  245. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  246. simo/notifications/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  247. simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-38.pyc +0 -0
  248. simo/notifications/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  249. simo/users/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  250. simo/users/migrations/__pycache__/0002_componentpermission.cpython-38.pyc +0 -0
  251. simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-38.pyc +0 -0
  252. simo/users/migrations/__pycache__/0004_user_secret_key.cpython-38.pyc +0 -0
  253. simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-38.pyc +0 -0
  254. simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-38.pyc +0 -0
  255. simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-38.pyc +0 -0
  256. simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-38.pyc +0 -0
  257. simo/users/migrations/__pycache__/0009_remove_user_role.cpython-38.pyc +0 -0
  258. simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-38.pyc +0 -0
  259. simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-38.pyc +0 -0
  260. simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-38.pyc +0 -0
  261. simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-38.pyc +0 -0
  262. simo/users/migrations/__pycache__/0014_user_roles.cpython-38.pyc +0 -0
  263. simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-38.pyc +0 -0
  264. simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-38.pyc +0 -0
  265. simo/users/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  266. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/LICENSE.md +0 -0
  267. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,391 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ from collections import OrderedDict
3
+
4
+ import six
5
+ from django import forms
6
+ from rest_framework import serializers
7
+
8
+ from .. import fields
9
+ from ..utils import (
10
+ find_matching_class_kwargs,
11
+ get_attr_from_base_classes,
12
+ get_class_name_with_new_suffix,
13
+ reduce_attr_dict_from_instance,
14
+ )
15
+
16
+
17
+ class FormSerializerFailure(object):
18
+ """
19
+ Enum for the possible form validation failure modes.
20
+
21
+ 'fail': validation failures should be added to self.errors
22
+ and `is_valid()` should return False.
23
+
24
+ 'drop': validation failures for a given attribute will result in
25
+ that attribute being dropped from `cleaned_data`;
26
+ `is_valid()` will return True.
27
+
28
+ 'ignore': validation failures will be ignored, and the (invalid)
29
+ data provided will be preserved in `cleaned_data`.
30
+ """
31
+ fail = 'fail'
32
+ drop = 'drop'
33
+ ignore = 'ignore'
34
+
35
+
36
+ class FormSerializerFieldMixin(object):
37
+ def run_validation(self, data):
38
+ try:
39
+ return super(FormSerializerFieldMixin, self).run_validation(data)
40
+ except (serializers.ValidationError, forms.ValidationError) as e:
41
+ # Only handle a ValidationError if the full validation is
42
+ # requested or if field is in minimum required in the case
43
+ # of partial validation.
44
+ if any([not self.parent.partial,
45
+ self.parent.Meta.failure_mode == FormSerializerFailure.fail,
46
+ self.field_name in self.parent.Meta.minimum_required]):
47
+ raise
48
+ self.capture_failed_field(self.field_name, data, e.detail)
49
+ raise serializers.SkipField
50
+
51
+ def capture_failed_field(self, field_name, field_data, error_msg):
52
+ """
53
+ Hook for capturing invalid fields. This is used to track which fields have been skipped.
54
+
55
+ Args:
56
+ field_name (str): the name of the field whose data failed to validate
57
+ field_data (object): the data of the field that failed validation
58
+ error_msg (str): validation error message
59
+
60
+ Returns:
61
+ Not meant to return anything.
62
+ """
63
+
64
+
65
+ def make_form_serializer_field(field_class, validation_form_serializer_field_mixin_class=FormSerializerFieldMixin):
66
+ return type(
67
+ get_class_name_with_new_suffix(field_class, 'Field', 'FormSerializerField'),
68
+ (validation_form_serializer_field_mixin_class, field_class,),
69
+ {}
70
+ )
71
+
72
+
73
+ FORM_SERIALIZER_FIELD_MAPPING = {
74
+ forms.CharField: make_form_serializer_field(fields.CharField),
75
+ forms.MultipleChoiceField: make_form_serializer_field(fields.ChoiceField),
76
+ forms.ChoiceField: make_form_serializer_field(fields.ChoiceField),
77
+ forms.BooleanField: make_form_serializer_field(fields.BooleanField),
78
+ forms.IntegerField: make_form_serializer_field(fields.IntegerField),
79
+ forms.EmailField: make_form_serializer_field(fields.EmailField),
80
+ forms.DateTimeField: make_form_serializer_field(fields.DateTimeField),
81
+ forms.DateField: make_form_serializer_field(fields.DateField),
82
+ forms.TimeField: make_form_serializer_field(fields.TimeField),
83
+ }
84
+
85
+
86
+ class FormSerializerOptions(object):
87
+ """
88
+ Defines what options FormSerializer can have in Meta.
89
+
90
+ :param form: The ``django.form.Form`` class to use as the base
91
+ for the serializer.
92
+ :param failure_mode: `FormSerializerFailure`
93
+ :param minimum_required: the minimum required fields that
94
+ must validate in order for validation to succeed.
95
+ """
96
+
97
+ def __init__(self, meta, class_name):
98
+ self.form = getattr(meta, 'form', None)
99
+ self.failure_mode = getattr(meta, 'failure_mode', FormSerializerFailure.fail)
100
+ self.minimum_required = getattr(meta, 'minimum_required', [])
101
+ self.field_mapping = getattr(meta, 'field_mapping', {})
102
+ self.exclude = getattr(meta, 'exclude', [])
103
+
104
+ assert self.form, (
105
+ 'Class {serializer_class} missing "Meta.form" attribute'.format(
106
+ serializer_class=class_name
107
+ )
108
+ )
109
+ assert self.failure_mode in vars(FormSerializerFailure).values(), (
110
+ 'Failure mode "{}" is not supported'.format(self.failure_mode)
111
+ )
112
+ if self.failure_mode == FormSerializerFailure.ignore:
113
+ raise NotImplementedError(
114
+ 'Failure mode "{}" is not supported since it is not clear '
115
+ 'what is an expected behavior'.format(self.failure_mode)
116
+ )
117
+
118
+ # copy all other custom keys
119
+ for k, v in vars(meta).items():
120
+ if hasattr(self, k):
121
+ continue
122
+ setattr(self, k, v)
123
+
124
+
125
+ class FormSerializerMeta(serializers.SerializerMetaclass):
126
+ def __new__(cls, name, bases, attrs):
127
+ try:
128
+ parents = [b for b in bases if issubclass(b, FormSerializer)]
129
+ except NameError:
130
+ # We are defining FormSerializer itself
131
+ parents = None
132
+
133
+ if not parents or attrs.pop('_is_base', False):
134
+ return super(FormSerializerMeta, cls).__new__(cls, name, bases, attrs)
135
+
136
+ assert 'Meta' in attrs, (
137
+ 'Class {serializer_class} missing "Meta" attribute'.format(
138
+ serializer_class=name
139
+ )
140
+ )
141
+ options_class = get_attr_from_base_classes(
142
+ bases, attrs, '_options_class', default=FormSerializerOptions
143
+ )
144
+ attrs['Meta'] = options_class(attrs['Meta'], name)
145
+
146
+ return super(FormSerializerMeta, cls).__new__(cls, name, bases, attrs)
147
+
148
+
149
+ class FormSerializerBase(serializers.Serializer):
150
+ """
151
+ The base Form serializer class.
152
+ When a subclassing serializer is validated or saved, this will
153
+ pass-through those operations to the mapped Form.
154
+ """
155
+ _is_base = True
156
+ _options_class = FormSerializerOptions
157
+
158
+ def __init__(self, *args, **kwargs):
159
+ # We override partial validation handling, since for
160
+ # it to be properly implemented for a Form the caller
161
+ # must also choose whether or not to include the data
162
+ # that failed validation in the result cleaned_data.
163
+ # Unfortunately there is no way to prevent a caller from
164
+ # sending this param themselves, because of the way DRFv2
165
+ # serializers work internally.
166
+ if self.Meta.failure_mode != FormSerializerFailure.fail:
167
+ kwargs['partial'] = True
168
+
169
+ self.form_instance = None
170
+
171
+ super(FormSerializerBase, self).__init__(*args, **kwargs)
172
+
173
+ def get_form(self, data=None, **kwargs):
174
+ """
175
+ Create an instance of configured form class.
176
+
177
+ :param data: optional initial data
178
+ :param kwargs: key args to pass to form instance
179
+ :return: instance of `self.opts.form`, bound if data was provided,
180
+ otherwise unbound.
181
+ """
182
+ form_cls = self.Meta.form
183
+
184
+ instance = form_cls(data=data, **kwargs)
185
+
186
+ # Handle partial validation on the form side
187
+ if self.partial:
188
+ set_form_partial_validation(
189
+ instance, self.Meta.minimum_required
190
+ )
191
+
192
+ return instance
193
+
194
+ def get_fields(self):
195
+ """
196
+ Return all the fields that should be serialized for the form.
197
+ This is a hook provided by parent class.
198
+ :return: dict of {'field_name': serializer_field_instance}
199
+ """
200
+ ret = super(FormSerializerBase, self).get_fields()
201
+
202
+ field_mapping = reduce_attr_dict_from_instance(
203
+ self,
204
+ lambda i: getattr(getattr(i, 'Meta', None), 'field_mapping', {}),
205
+ FORM_SERIALIZER_FIELD_MAPPING
206
+ )
207
+
208
+ # Iterate over the form fields, creating an
209
+ # instance of serializer field for each.
210
+ form = self.Meta.form
211
+ for field_name, form_field in getattr(form, 'all_base_fields', form.base_fields).items():
212
+ # if field is specified as excluded field
213
+ if field_name in getattr(self.Meta, 'exclude', []):
214
+ continue
215
+
216
+ # if field is already defined via declared fields
217
+ # skip mapping it from forms which then honors
218
+ # the custom validation defined on the DRF declared field
219
+ if field_name in ret:
220
+ continue
221
+
222
+ try:
223
+ serializer_field_class = field_mapping[form_field.__class__]
224
+ except KeyError:
225
+ raise TypeError(
226
+ "{field} is not mapped to a serializer field. "
227
+ "Please add {field} to {serializer}.Meta.field_mapping. "
228
+ "Currently mapped fields: {mapped}".format(
229
+ field=form_field.__class__.__name__,
230
+ serializer=self.__class__.__name__,
231
+ mapped=', '.join(sorted([i.__name__ for i in field_mapping.keys()]))
232
+ )
233
+ )
234
+ else:
235
+ ret[field_name] = self._get_field(form_field, serializer_field_class)
236
+
237
+ return ret
238
+
239
+ def _get_field(self, form_field, serializer_field_class):
240
+ kwargs = self._get_field_kwargs(form_field, serializer_field_class)
241
+
242
+ field = serializer_field_class(**kwargs)
243
+
244
+ for kwarg, value in kwargs.items():
245
+ # set corresponding DRF attributes which don't have alternative
246
+ # in Django form fields
247
+ if kwarg == 'required':
248
+ field.allow_blank = not value
249
+ field.allow_null = not value
250
+
251
+ # ChoiceField natively uses choice_strings_to_values
252
+ # in the to_internal_value flow
253
+ elif kwarg == 'choices':
254
+ field.choice_strings_to_values = {
255
+ six.text_type(key): key for key in OrderedDict(value).keys()
256
+ }
257
+
258
+ return field
259
+
260
+ def _get_field_kwargs(self, form_field, serializer_field_class):
261
+ """
262
+ For a given Form field, determine what validation attributes
263
+ have been set. Includes things like max_length, required, etc.
264
+ These will be used to create an instance of ``rest_framework.fields.Field``.
265
+
266
+ :param form_field: a ``django.forms.field.Field`` instance
267
+ :return: dictionary of attributes to set
268
+ """
269
+ attrs = find_matching_class_kwargs(form_field, serializer_field_class)
270
+
271
+ if 'choices' in attrs:
272
+ choices = OrderedDict(attrs['choices']).keys()
273
+ attrs['choices'] = OrderedDict(zip(choices, choices))
274
+
275
+ if getattr(form_field, 'initial', None):
276
+ attrs['default'] = form_field.initial
277
+
278
+ # avoid "May not set both `required` and `default`"
279
+ if attrs.get('required') and 'default' in attrs:
280
+ del attrs['required']
281
+
282
+ return attrs
283
+
284
+ def validate(self, data):
285
+ """
286
+ Validate a form instance using the data that has been run through
287
+ the serializer field validation.
288
+
289
+ :param data: deserialized data to validate
290
+ :return: validated, cleaned form data
291
+ :raise: ``django.core.exceptions.ValidationError`` on failed
292
+ validation.
293
+ """
294
+ self.form_instance = form = self.get_form(data=data)
295
+
296
+ if not form.is_valid():
297
+ _cleaned_data = getattr(form, 'cleaned_data', None) or {}
298
+
299
+ if self.Meta.failure_mode == FormSerializerFailure.fail:
300
+ raise serializers.ValidationError(form.errors)
301
+
302
+ else:
303
+ self.capture_failed_fields(data, form.errors)
304
+ cleaned_data = {k: v for k, v in data.items() if k not in form.errors}
305
+ # use any cleaned data form might of validated right until
306
+ # this moment even if validation failed
307
+ cleaned_data.update(_cleaned_data)
308
+
309
+ else:
310
+ cleaned_data = form.cleaned_data
311
+
312
+ return cleaned_data
313
+
314
+ def to_representation(self, instance):
315
+ """
316
+ It doesn't make much sense to serialize a Form instance to JSON.
317
+ """
318
+ raise NotImplementedError(
319
+ '{} does not currently serialize Form --> JSON'
320
+ ''.format(self.__class__.__name__)
321
+ )
322
+
323
+ def capture_failed_fields(self, raw_data, form_errors):
324
+ """
325
+ Hook for capturing all failed form data when the failure mode is not FormSerializerFailure.fail
326
+
327
+ Args:
328
+ raw_data (dict): raw form data
329
+ form_errors (dict): all form errors
330
+
331
+ Returns:
332
+ Not meant to return anything.
333
+ """
334
+
335
+
336
+ class FormSerializer(six.with_metaclass(FormSerializerMeta, FormSerializerBase)):
337
+ pass
338
+
339
+
340
+ class LazyLoadingValidationsMixin(object):
341
+ """
342
+ Provides a method for re-evaluating the validations for
343
+ a form using an instance of it (whereas the FormSerializer
344
+ only uses the form class).
345
+ If your form class loads validations in `__init__()`, you
346
+ need this.
347
+ """
348
+
349
+ def repopulate_form_fields(self):
350
+ """
351
+ Repopulate the form fields, update choices.
352
+ The repopulation is required b/c some DT forms use a lazy-load approach
353
+ to populating choices of a ChoiceField, by putting the load
354
+ in the form's constructor. Also, the DT fields may require context_data,
355
+ which is unavailable when the fields are first constructed
356
+ (which happens during evaluation of the serializer classes).
357
+ :return: None
358
+ """
359
+ instance = self.get_form()
360
+
361
+ for form_field_name, form_field in getattr(instance, 'all_fields', instance.fields).items():
362
+ if hasattr(form_field, 'choices'):
363
+ # let drf normalize choices down to key: key
364
+ # key:value is unsupported unlike in django form fields
365
+ self.fields[form_field_name].choices = OrderedDict(form_field.choices).keys()
366
+ self.fields[form_field_name].choice_strings_to_values = {
367
+ six.text_type(key): key for key in OrderedDict(form_field.choices).keys()
368
+ }
369
+
370
+ def to_internal_value(self, data):
371
+ """
372
+ We have tons of "choices" loading in form `__init__()`,
373
+ (so that DB query is evaluated at last possible moment) so require the
374
+ use of ``common.common_json.serializers.LazyLoadingValidationsMixin``.
375
+ """
376
+ self.repopulate_form_fields()
377
+ return super(LazyLoadingValidationsMixin, self).to_internal_value(data)
378
+
379
+
380
+ def set_form_partial_validation(form, minimum_required):
381
+ """
382
+ Get a form ready for partial validation.
383
+ For all fields not in `minimum_required`, set
384
+ `Field.required` to False.
385
+
386
+ :param minimum_required: list of minimum required fields
387
+ :return: None
388
+ """
389
+ for field_name, field in getattr(form, 'all_fields', form.fields).items():
390
+ if field_name not in minimum_required:
391
+ field.required = False
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import absolute_import, print_function, unicode_literals
3
+ import copy
4
+
5
+ from rest_framework.serializers import BaseSerializer, ListSerializer
6
+
7
+
8
+ class SwappingSerializerMixin(BaseSerializer):
9
+ """
10
+ Declaratively swap any of descendant fields.
11
+
12
+ Useful when any of child serializers need to be swapped to similar but slightly different
13
+ serializer. One use case is to swap normal serializer to hyperlinked one...
14
+
15
+ For example::
16
+
17
+ class SwappedSerializer(SwappingSerializerMixin, MyBaseSerializer):
18
+ class Meta(MyBaseSerializer.Meta):
19
+ swappable_fields = {
20
+ MySerializer: MyOtherSerializer,
21
+ }
22
+
23
+ .. note::
24
+ ``MyOtherSerializer`` will be instantiated with same ``*args, **kwargs`` as given to ``MySerializer``.
25
+ This allows to swap fields but to leave state as is.
26
+ """
27
+ def __init__(self, *args, **kwargs):
28
+ super(SwappingSerializerMixin, self).__init__(*args, **kwargs)
29
+ self.swap_fields(self)
30
+
31
+ def swap_fields(self, serializer):
32
+ for name, field in list(serializer.fields.items()):
33
+ new_field = self.swap_field(field)
34
+ if new_field is not field:
35
+ serializer.fields[name] = new_field
36
+ if isinstance(new_field, ListSerializer):
37
+ self.swap_fields(new_field.child)
38
+ elif isinstance(new_field, BaseSerializer):
39
+ self.swap_fields(new_field)
40
+ return serializer
41
+
42
+ def swap_field(self, field):
43
+ replacement = getattr(self.Meta, "swappable_fields", {}).get(field.__class__)
44
+ if replacement is None:
45
+ return field
46
+
47
+ field_copy = copy.deepcopy(field)
48
+ return replacement(*field_copy._args, **field_copy._kwargs)
File without changes
File without changes
@@ -0,0 +1,94 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import unittest
3
+ from collections import OrderedDict
4
+ from decimal import ROUND_DOWN, Decimal
5
+
6
+ import mock
7
+ import pytz
8
+
9
+ from ...fields.custom import (
10
+ NonValidatingChoiceField,
11
+ PositiveIntegerField,
12
+ RoundedDecimalField,
13
+ UTCDateTimeField,
14
+ UnvalidatedField,
15
+ )
16
+
17
+
18
+ class TestUnvalidatedField(unittest.TestCase):
19
+ def test_run_validators(self):
20
+ validator = mock.MagicMock()
21
+ field = UnvalidatedField(validators=[validator])
22
+
23
+ actual = field.run_validators(mock.sentinel.value)
24
+
25
+ self.assertIsNone(actual)
26
+ self.assertFalse(validator.called)
27
+
28
+
29
+ class TestPositiveIntegerField(unittest.TestCase):
30
+ def test_init_default(self):
31
+ field = PositiveIntegerField()
32
+ self.assertEqual(field.min_value, 0)
33
+
34
+ def test_init_passed(self):
35
+ field = PositiveIntegerField(min_value=mock.sentinel.min_value)
36
+ self.assertEqual(field.min_value, mock.sentinel.min_value)
37
+
38
+
39
+ class TestUTCDateTimeField(unittest.TestCase):
40
+ def test_init(self):
41
+ field = UTCDateTimeField()
42
+
43
+ self.assertEqual(
44
+ getattr(field, 'timezone', getattr(field, 'default_timezone', None)),
45
+ pytz.UTC
46
+ )
47
+
48
+
49
+ class TestNonValidatingChoiceField(unittest.TestCase):
50
+ def test_init(self):
51
+ field = NonValidatingChoiceField()
52
+
53
+ self.assertEqual(field.choices, OrderedDict())
54
+
55
+ def test_to_internal_value(self):
56
+ field = NonValidatingChoiceField(choices=['bar'])
57
+
58
+ self.assertEqual(field.to_internal_value('bar'), 'bar')
59
+ self.assertEqual(field.to_internal_value('haha'), 'haha')
60
+
61
+
62
+ class TestRoundedDecimalField(unittest.TestCase):
63
+ def test_init(self):
64
+ field = RoundedDecimalField()
65
+ self.assertEqual(field.decimal_places, 2)
66
+ self.assertIsNone(field.rounding)
67
+
68
+ new_field = RoundedDecimalField(rounding=ROUND_DOWN)
69
+ self.assertEqual(new_field.rounding, ROUND_DOWN)
70
+
71
+ def test_to_internal_value(self):
72
+ field = RoundedDecimalField()
73
+ self.assertEqual(field.to_internal_value(5), Decimal('5'))
74
+ self.assertEqual(field.to_internal_value(5.2), Decimal('5.2'))
75
+ self.assertEqual(field.to_internal_value(5.23), Decimal('5.23'))
76
+ self.assertEqual(field.to_internal_value(5.2345), Decimal('5.23'))
77
+ self.assertEqual(field.to_internal_value(5.2356), Decimal('5.24'))
78
+ self.assertEqual(field.to_internal_value('5'), Decimal('5'))
79
+ self.assertEqual(field.to_internal_value('5.2'), Decimal('5.2'))
80
+ self.assertEqual(field.to_internal_value('5.23'), Decimal('5.23'))
81
+ self.assertEqual(field.to_internal_value('5.2345'), Decimal('5.23'))
82
+ self.assertEqual(field.to_internal_value('5.2356'), Decimal('5.24'))
83
+ self.assertEqual(field.to_internal_value(Decimal('5')), Decimal('5'))
84
+ self.assertEqual(field.to_internal_value(Decimal('5.2')), Decimal('5.2'))
85
+ self.assertEqual(field.to_internal_value(Decimal('5.23')), Decimal('5.23'))
86
+ self.assertEqual(field.to_internal_value(Decimal('5.2345')), Decimal('5.23'))
87
+ self.assertEqual(field.to_internal_value(Decimal('5.2356')), Decimal('5.24'))
88
+ self.assertEqual(field.to_internal_value(Decimal('4.2399')), Decimal('4.24'))
89
+
90
+ floored_field = RoundedDecimalField(rounding=ROUND_DOWN)
91
+ self.assertEqual(floored_field.to_internal_value(5.2345), Decimal('5.23'))
92
+ self.assertEqual(floored_field.to_internal_value(5.2356), Decimal('5.23'))
93
+ self.assertEqual(floored_field.to_internal_value(Decimal('5.2345')), Decimal('5.23'))
94
+ self.assertEqual(floored_field.to_internal_value(Decimal('5.2356')), Decimal('5.23'))
@@ -0,0 +1,13 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import unittest
3
+
4
+ from ...fields import _fields
5
+ from ...fields.mixins import AllowBlankNullFieldMixin, EmptyStringFieldMixin
6
+
7
+
8
+ class TestFields(unittest.TestCase):
9
+ def test_subclasses(self):
10
+ for f in _fields.FIELDS:
11
+ f = getattr(_fields, f)
12
+ self.assertTrue(issubclass(f, EmptyStringFieldMixin))
13
+ self.assertTrue(issubclass(f, AllowBlankNullFieldMixin))
@@ -0,0 +1,96 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import unittest
3
+
4
+ import mock
5
+ from rest_framework import fields
6
+
7
+ from ...fields.mixins import (
8
+ AllowBlankNullFieldMixin,
9
+ EmptyStringFieldMixin,
10
+ ValueAsTextFieldMixin,
11
+ )
12
+
13
+
14
+ class TestEmptyStringFieldMixin(unittest.TestCase):
15
+ def setUp(self):
16
+ super(TestEmptyStringFieldMixin, self).setUp()
17
+
18
+ class Field(EmptyStringFieldMixin, fields.IntegerField):
19
+ pass
20
+
21
+ self.field = Field()
22
+
23
+ def test_validate_empty_values_empty_string_required(self):
24
+ with self.assertRaises(fields.ValidationError):
25
+ self.field.validate_empty_values('')
26
+
27
+ def test_validate_empty_values_empty_string(self):
28
+ self.field.required = False
29
+
30
+ actual = self.field.validate_empty_values('')
31
+
32
+ self.assertTupleEqual(actual, (True, ''))
33
+
34
+ def test_validate_empty_values(self):
35
+ self.field.required = False
36
+ self.field.allow_null = True
37
+
38
+ actual = self.field.validate_empty_values(None)
39
+
40
+ self.assertTupleEqual(actual, (True, None))
41
+
42
+ def test_representation(self):
43
+ self.field.required = False
44
+
45
+ self.assertEqual(self.field.to_representation('50'), 50)
46
+ self.assertEqual(self.field.to_representation(''), '')
47
+
48
+ def test_representation_required(self):
49
+ self.field.required = True
50
+
51
+ self.assertEqual(self.field.to_representation('50'), 50)
52
+ with self.assertRaises(ValueError):
53
+ self.field.to_representation('')
54
+
55
+
56
+ class TestAllowBlankFieldMixin(unittest.TestCase):
57
+ def setUp(self):
58
+ super(TestAllowBlankFieldMixin, self).setUp()
59
+
60
+ class Field(AllowBlankNullFieldMixin, fields.CharField):
61
+ pass
62
+
63
+ self.field_class = Field
64
+
65
+ def test_init(self):
66
+ field = self.field_class(required=False)
67
+
68
+ self.assertTrue(field.allow_blank)
69
+ self.assertTrue(field.allow_null)
70
+
71
+
72
+ class TestValueAsTextFieldMixin(unittest.TestCase):
73
+ def setUp(self):
74
+ super(TestValueAsTextFieldMixin, self).setUp()
75
+
76
+ class Field(ValueAsTextFieldMixin, fields.IntegerField):
77
+ pass
78
+
79
+ self.field = Field(required=False, allow_null=True, max_value=100)
80
+
81
+ def test_to_string_value(self):
82
+ self.assertIsNone(self.field.to_string_value(None))
83
+ self.assertEqual(self.field.to_string_value(5), '5')
84
+
85
+ def test_prepare_value_for_validation(self):
86
+ self.assertEqual(
87
+ self.field.prepare_value_for_validation(mock.sentinel.value),
88
+ mock.sentinel.value
89
+ )
90
+
91
+ def test_run_validation(self):
92
+ self.assertIsNone(self.field.run_validation(None))
93
+
94
+ self.assertEqual(self.field.run_validation(50), '50')
95
+ with self.assertRaises(fields.ValidationError):
96
+ self.field.run_validation(500)
@@ -0,0 +1,40 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import unittest
3
+ from decimal import Decimal
4
+
5
+ import pytz
6
+ from django.test.utils import override_settings
7
+
8
+ from ...fields.modified import BooleanField, DateTimeField, DecimalField
9
+
10
+
11
+ class TestBooleanField(unittest.TestCase):
12
+ def test_init(self):
13
+ field = BooleanField(true_values=['Y', 'Yes'], false_values=['N', 'No'])
14
+ self.assertIn('Y', field.TRUE_VALUES)
15
+ self.assertIn('Yes', field.TRUE_VALUES)
16
+ self.assertIn('N', field.FALSE_VALUES)
17
+ self.assertIn('No', field.FALSE_VALUES)
18
+
19
+
20
+ class TestDecimalField(unittest.TestCase):
21
+ def test_init(self):
22
+ field = DecimalField()
23
+ self.assertIsNone(field.max_digits)
24
+ self.assertIsNone(field.decimal_places)
25
+
26
+ def test_quantize(self):
27
+ field = DecimalField()
28
+ self.assertIsNone(field.quantize(None))
29
+
30
+ field = DecimalField(max_digits=4, decimal_places=3)
31
+ self.assertEqual(field.quantize(Decimal('5.1234567')), Decimal('5.123'))
32
+
33
+
34
+ class TestDateTimeField(unittest.TestCase):
35
+ @override_settings(USE_TZ=True)
36
+ def test_init(self):
37
+ value = '2015-01-02T16:00'
38
+
39
+ self.assertIsNotNone(DateTimeField(default_timezone=pytz.utc).run_validation(value).tzinfo)
40
+ self.assertIsNone(DateTimeField(default_timezone=None).run_validation(value).tzinfo)
File without changes