udata 9.1.2.dev30355__py2.py3-none-any.whl → 9.1.2.dev30454__py2.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 udata might be problematic. Click here for more details.

Files changed (413) hide show
  1. tasks/__init__.py +109 -107
  2. tasks/helpers.py +18 -18
  3. udata/__init__.py +4 -4
  4. udata/admin/views.py +5 -5
  5. udata/api/__init__.py +111 -134
  6. udata/api/commands.py +45 -37
  7. udata/api/errors.py +5 -4
  8. udata/api/fields.py +23 -21
  9. udata/api/oauth2.py +55 -74
  10. udata/api/parsers.py +15 -15
  11. udata/api/signals.py +1 -1
  12. udata/api_fields.py +137 -89
  13. udata/app.py +58 -55
  14. udata/assets.py +5 -5
  15. udata/auth/__init__.py +37 -26
  16. udata/auth/forms.py +23 -15
  17. udata/auth/helpers.py +1 -1
  18. udata/auth/mails.py +3 -3
  19. udata/auth/password_validation.py +19 -15
  20. udata/auth/views.py +94 -68
  21. udata/commands/__init__.py +71 -69
  22. udata/commands/cache.py +7 -7
  23. udata/commands/db.py +201 -140
  24. udata/commands/dcat.py +36 -30
  25. udata/commands/fixtures.py +100 -84
  26. udata/commands/images.py +21 -20
  27. udata/commands/info.py +17 -20
  28. udata/commands/init.py +10 -10
  29. udata/commands/purge.py +12 -13
  30. udata/commands/serve.py +41 -29
  31. udata/commands/static.py +16 -18
  32. udata/commands/test.py +20 -20
  33. udata/commands/tests/fixtures.py +26 -24
  34. udata/commands/worker.py +31 -33
  35. udata/core/__init__.py +12 -12
  36. udata/core/activity/__init__.py +0 -1
  37. udata/core/activity/api.py +59 -49
  38. udata/core/activity/models.py +28 -26
  39. udata/core/activity/signals.py +1 -1
  40. udata/core/activity/tasks.py +16 -10
  41. udata/core/badges/api.py +6 -6
  42. udata/core/badges/commands.py +14 -13
  43. udata/core/badges/fields.py +8 -5
  44. udata/core/badges/forms.py +7 -4
  45. udata/core/badges/models.py +16 -31
  46. udata/core/badges/permissions.py +1 -3
  47. udata/core/badges/signals.py +2 -2
  48. udata/core/badges/tasks.py +3 -2
  49. udata/core/badges/tests/test_commands.py +10 -10
  50. udata/core/badges/tests/test_model.py +24 -31
  51. udata/core/contact_point/api.py +19 -18
  52. udata/core/contact_point/api_fields.py +21 -14
  53. udata/core/contact_point/factories.py +2 -2
  54. udata/core/contact_point/forms.py +7 -6
  55. udata/core/contact_point/models.py +3 -5
  56. udata/core/dataservices/api.py +26 -21
  57. udata/core/dataservices/factories.py +13 -11
  58. udata/core/dataservices/models.py +35 -40
  59. udata/core/dataservices/permissions.py +4 -4
  60. udata/core/dataservices/rdf.py +40 -17
  61. udata/core/dataservices/tasks.py +4 -3
  62. udata/core/dataset/actions.py +10 -10
  63. udata/core/dataset/activities.py +21 -23
  64. udata/core/dataset/api.py +321 -298
  65. udata/core/dataset/api_fields.py +443 -271
  66. udata/core/dataset/apiv2.py +305 -229
  67. udata/core/dataset/commands.py +38 -36
  68. udata/core/dataset/constants.py +61 -54
  69. udata/core/dataset/csv.py +70 -74
  70. udata/core/dataset/events.py +39 -32
  71. udata/core/dataset/exceptions.py +8 -4
  72. udata/core/dataset/factories.py +57 -65
  73. udata/core/dataset/forms.py +87 -63
  74. udata/core/dataset/models.py +336 -280
  75. udata/core/dataset/permissions.py +9 -6
  76. udata/core/dataset/preview.py +15 -17
  77. udata/core/dataset/rdf.py +156 -122
  78. udata/core/dataset/search.py +92 -77
  79. udata/core/dataset/signals.py +1 -1
  80. udata/core/dataset/tasks.py +63 -54
  81. udata/core/discussions/actions.py +5 -5
  82. udata/core/discussions/api.py +124 -120
  83. udata/core/discussions/factories.py +2 -2
  84. udata/core/discussions/forms.py +9 -7
  85. udata/core/discussions/metrics.py +1 -3
  86. udata/core/discussions/models.py +25 -24
  87. udata/core/discussions/notifications.py +18 -14
  88. udata/core/discussions/permissions.py +3 -3
  89. udata/core/discussions/signals.py +4 -4
  90. udata/core/discussions/tasks.py +24 -28
  91. udata/core/followers/api.py +32 -33
  92. udata/core/followers/models.py +9 -9
  93. udata/core/followers/signals.py +3 -3
  94. udata/core/jobs/actions.py +7 -7
  95. udata/core/jobs/api.py +99 -92
  96. udata/core/jobs/commands.py +48 -49
  97. udata/core/jobs/forms.py +11 -11
  98. udata/core/jobs/models.py +6 -6
  99. udata/core/metrics/__init__.py +2 -2
  100. udata/core/metrics/commands.py +34 -30
  101. udata/core/metrics/models.py +2 -4
  102. udata/core/metrics/signals.py +1 -1
  103. udata/core/metrics/tasks.py +3 -3
  104. udata/core/organization/activities.py +12 -15
  105. udata/core/organization/api.py +167 -174
  106. udata/core/organization/api_fields.py +183 -124
  107. udata/core/organization/apiv2.py +32 -32
  108. udata/core/organization/commands.py +20 -22
  109. udata/core/organization/constants.py +11 -11
  110. udata/core/organization/csv.py +17 -15
  111. udata/core/organization/factories.py +8 -11
  112. udata/core/organization/forms.py +32 -26
  113. udata/core/organization/metrics.py +2 -1
  114. udata/core/organization/models.py +87 -67
  115. udata/core/organization/notifications.py +18 -14
  116. udata/core/organization/permissions.py +10 -11
  117. udata/core/organization/rdf.py +14 -14
  118. udata/core/organization/search.py +30 -28
  119. udata/core/organization/signals.py +7 -7
  120. udata/core/organization/tasks.py +42 -61
  121. udata/core/owned.py +38 -27
  122. udata/core/post/api.py +82 -81
  123. udata/core/post/constants.py +8 -5
  124. udata/core/post/factories.py +4 -4
  125. udata/core/post/forms.py +13 -14
  126. udata/core/post/models.py +20 -22
  127. udata/core/post/tests/test_api.py +30 -32
  128. udata/core/reports/api.py +8 -7
  129. udata/core/reports/constants.py +1 -3
  130. udata/core/reports/models.py +10 -10
  131. udata/core/reuse/activities.py +15 -19
  132. udata/core/reuse/api.py +123 -126
  133. udata/core/reuse/api_fields.py +120 -85
  134. udata/core/reuse/apiv2.py +11 -10
  135. udata/core/reuse/constants.py +23 -23
  136. udata/core/reuse/csv.py +18 -18
  137. udata/core/reuse/factories.py +5 -9
  138. udata/core/reuse/forms.py +24 -21
  139. udata/core/reuse/models.py +55 -51
  140. udata/core/reuse/permissions.py +2 -2
  141. udata/core/reuse/search.py +49 -46
  142. udata/core/reuse/signals.py +1 -1
  143. udata/core/reuse/tasks.py +4 -5
  144. udata/core/site/api.py +47 -50
  145. udata/core/site/factories.py +2 -2
  146. udata/core/site/forms.py +4 -5
  147. udata/core/site/models.py +94 -63
  148. udata/core/site/rdf.py +14 -14
  149. udata/core/spam/api.py +16 -9
  150. udata/core/spam/constants.py +4 -4
  151. udata/core/spam/fields.py +13 -7
  152. udata/core/spam/models.py +27 -20
  153. udata/core/spam/signals.py +1 -1
  154. udata/core/spam/tests/test_spam.py +6 -5
  155. udata/core/spatial/api.py +72 -80
  156. udata/core/spatial/api_fields.py +73 -58
  157. udata/core/spatial/commands.py +67 -64
  158. udata/core/spatial/constants.py +3 -3
  159. udata/core/spatial/factories.py +37 -54
  160. udata/core/spatial/forms.py +27 -26
  161. udata/core/spatial/geoids.py +17 -17
  162. udata/core/spatial/models.py +43 -47
  163. udata/core/spatial/tasks.py +2 -1
  164. udata/core/spatial/tests/test_api.py +115 -130
  165. udata/core/spatial/tests/test_fields.py +74 -77
  166. udata/core/spatial/tests/test_geoid.py +22 -22
  167. udata/core/spatial/tests/test_models.py +5 -7
  168. udata/core/spatial/translations.py +16 -16
  169. udata/core/storages/__init__.py +16 -18
  170. udata/core/storages/api.py +66 -64
  171. udata/core/storages/tasks.py +7 -7
  172. udata/core/storages/utils.py +15 -15
  173. udata/core/storages/views.py +5 -6
  174. udata/core/tags/api.py +17 -14
  175. udata/core/tags/csv.py +4 -4
  176. udata/core/tags/models.py +8 -5
  177. udata/core/tags/tasks.py +11 -13
  178. udata/core/tags/views.py +4 -4
  179. udata/core/topic/api.py +84 -73
  180. udata/core/topic/apiv2.py +157 -127
  181. udata/core/topic/factories.py +3 -4
  182. udata/core/topic/forms.py +12 -14
  183. udata/core/topic/models.py +14 -19
  184. udata/core/topic/parsers.py +26 -26
  185. udata/core/user/activities.py +30 -29
  186. udata/core/user/api.py +151 -152
  187. udata/core/user/api_fields.py +132 -100
  188. udata/core/user/apiv2.py +7 -7
  189. udata/core/user/commands.py +38 -38
  190. udata/core/user/factories.py +8 -9
  191. udata/core/user/forms.py +14 -11
  192. udata/core/user/metrics.py +2 -2
  193. udata/core/user/models.py +68 -69
  194. udata/core/user/permissions.py +4 -5
  195. udata/core/user/rdf.py +7 -8
  196. udata/core/user/tasks.py +2 -2
  197. udata/core/user/tests/test_user_model.py +24 -16
  198. udata/cors.py +99 -0
  199. udata/db/tasks.py +2 -1
  200. udata/entrypoints.py +35 -31
  201. udata/errors.py +2 -1
  202. udata/event/values.py +6 -6
  203. udata/factories.py +2 -2
  204. udata/features/identicon/api.py +5 -6
  205. udata/features/identicon/backends.py +48 -55
  206. udata/features/identicon/tests/test_backends.py +4 -5
  207. udata/features/notifications/__init__.py +0 -1
  208. udata/features/notifications/actions.py +9 -9
  209. udata/features/notifications/api.py +17 -13
  210. udata/features/territories/__init__.py +12 -10
  211. udata/features/territories/api.py +14 -15
  212. udata/features/territories/models.py +23 -28
  213. udata/features/transfer/actions.py +8 -11
  214. udata/features/transfer/api.py +84 -77
  215. udata/features/transfer/factories.py +2 -1
  216. udata/features/transfer/models.py +11 -12
  217. udata/features/transfer/notifications.py +19 -15
  218. udata/features/transfer/permissions.py +5 -5
  219. udata/forms/__init__.py +5 -2
  220. udata/forms/fields.py +164 -172
  221. udata/forms/validators.py +19 -22
  222. udata/forms/widgets.py +9 -13
  223. udata/frontend/__init__.py +31 -26
  224. udata/frontend/csv.py +68 -58
  225. udata/frontend/markdown.py +40 -44
  226. udata/harvest/actions.py +89 -77
  227. udata/harvest/api.py +294 -238
  228. udata/harvest/backends/__init__.py +4 -4
  229. udata/harvest/backends/base.py +128 -111
  230. udata/harvest/backends/dcat.py +80 -66
  231. udata/harvest/commands.py +56 -60
  232. udata/harvest/csv.py +8 -8
  233. udata/harvest/exceptions.py +6 -3
  234. udata/harvest/filters.py +24 -23
  235. udata/harvest/forms.py +27 -28
  236. udata/harvest/models.py +88 -80
  237. udata/harvest/notifications.py +15 -10
  238. udata/harvest/signals.py +13 -13
  239. udata/harvest/tasks.py +11 -10
  240. udata/harvest/tests/factories.py +23 -24
  241. udata/harvest/tests/test_actions.py +136 -166
  242. udata/harvest/tests/test_api.py +220 -214
  243. udata/harvest/tests/test_base_backend.py +117 -112
  244. udata/harvest/tests/test_dcat_backend.py +380 -308
  245. udata/harvest/tests/test_filters.py +33 -22
  246. udata/harvest/tests/test_models.py +11 -14
  247. udata/harvest/tests/test_notifications.py +6 -7
  248. udata/harvest/tests/test_tasks.py +7 -6
  249. udata/i18n.py +237 -78
  250. udata/linkchecker/backends.py +5 -11
  251. udata/linkchecker/checker.py +23 -22
  252. udata/linkchecker/commands.py +4 -6
  253. udata/linkchecker/models.py +6 -6
  254. udata/linkchecker/tasks.py +18 -20
  255. udata/mail.py +21 -21
  256. udata/migrations/2020-07-24-remove-s-from-scope-oauth.py +9 -8
  257. udata/migrations/2020-08-24-add-fs-filename.py +9 -8
  258. udata/migrations/2020-09-28-update-reuses-datasets-metrics.py +5 -4
  259. udata/migrations/2020-10-16-migrate-ods-resources.py +9 -10
  260. udata/migrations/2021-04-08-update-schema-with-new-structure.py +8 -7
  261. udata/migrations/2021-05-27-fix-default-schema-name.py +7 -6
  262. udata/migrations/2021-07-05-remove-unused-badges.py +17 -15
  263. udata/migrations/2021-07-07-update-schema-for-community-resources.py +7 -6
  264. udata/migrations/2021-08-17-follow-integrity.py +5 -4
  265. udata/migrations/2021-08-17-harvest-integrity.py +13 -12
  266. udata/migrations/2021-08-17-oauth2client-integrity.py +5 -4
  267. udata/migrations/2021-08-17-transfer-integrity.py +5 -4
  268. udata/migrations/2021-08-17-users-integrity.py +9 -8
  269. udata/migrations/2021-12-14-reuse-topics.py +7 -6
  270. udata/migrations/2022-04-21-improve-extension-detection.py +8 -7
  271. udata/migrations/2022-09-22-clean-inactive-harvest-datasets.py +16 -14
  272. udata/migrations/2022-10-10-add-fs_uniquifier-to-user-model.py +6 -6
  273. udata/migrations/2022-10-10-migrate-harvest-extras.py +36 -26
  274. udata/migrations/2023-02-08-rename-internal-dates.py +46 -28
  275. udata/migrations/2024-01-29-fix-reuse-and-dataset-with-private-None.py +10 -8
  276. udata/migrations/2024-03-22-migrate-activity-kwargs-to-extras.py +6 -4
  277. udata/migrations/2024-06-11-fix-reuse-datasets-references.py +7 -6
  278. udata/migrations/__init__.py +123 -105
  279. udata/models/__init__.py +4 -4
  280. udata/mongo/__init__.py +13 -11
  281. udata/mongo/badges_field.py +3 -2
  282. udata/mongo/datetime_fields.py +13 -12
  283. udata/mongo/document.py +17 -16
  284. udata/mongo/engine.py +15 -16
  285. udata/mongo/errors.py +2 -1
  286. udata/mongo/extras_fields.py +30 -20
  287. udata/mongo/queryset.py +12 -12
  288. udata/mongo/slug_fields.py +38 -28
  289. udata/mongo/taglist_field.py +1 -2
  290. udata/mongo/url_field.py +5 -5
  291. udata/mongo/uuid_fields.py +4 -3
  292. udata/notifications/__init__.py +1 -1
  293. udata/notifications/mattermost.py +10 -9
  294. udata/rdf.py +167 -188
  295. udata/routing.py +40 -45
  296. udata/search/__init__.py +18 -19
  297. udata/search/adapter.py +17 -16
  298. udata/search/commands.py +44 -51
  299. udata/search/fields.py +13 -20
  300. udata/search/query.py +23 -18
  301. udata/search/result.py +9 -10
  302. udata/sentry.py +21 -19
  303. udata/settings.py +262 -198
  304. udata/sitemap.py +8 -6
  305. udata/storage/s3.py +20 -13
  306. udata/tags.py +4 -5
  307. udata/tasks.py +43 -42
  308. udata/tests/__init__.py +9 -6
  309. udata/tests/api/__init__.py +8 -6
  310. udata/tests/api/test_auth_api.py +395 -321
  311. udata/tests/api/test_base_api.py +33 -35
  312. udata/tests/api/test_contact_points.py +7 -9
  313. udata/tests/api/test_dataservices_api.py +211 -158
  314. udata/tests/api/test_datasets_api.py +823 -812
  315. udata/tests/api/test_follow_api.py +13 -15
  316. udata/tests/api/test_me_api.py +95 -112
  317. udata/tests/api/test_organizations_api.py +301 -339
  318. udata/tests/api/test_reports_api.py +35 -25
  319. udata/tests/api/test_reuses_api.py +134 -139
  320. udata/tests/api/test_swagger.py +5 -5
  321. udata/tests/api/test_tags_api.py +18 -25
  322. udata/tests/api/test_topics_api.py +94 -94
  323. udata/tests/api/test_transfer_api.py +53 -48
  324. udata/tests/api/test_user_api.py +128 -141
  325. udata/tests/apiv2/test_datasets.py +290 -198
  326. udata/tests/apiv2/test_me_api.py +10 -11
  327. udata/tests/apiv2/test_organizations.py +56 -74
  328. udata/tests/apiv2/test_swagger.py +5 -5
  329. udata/tests/apiv2/test_topics.py +69 -87
  330. udata/tests/cli/test_cli_base.py +8 -8
  331. udata/tests/cli/test_db_cli.py +21 -19
  332. udata/tests/dataservice/test_dataservice_tasks.py +8 -12
  333. udata/tests/dataset/test_csv_adapter.py +44 -35
  334. udata/tests/dataset/test_dataset_actions.py +2 -3
  335. udata/tests/dataset/test_dataset_commands.py +7 -8
  336. udata/tests/dataset/test_dataset_events.py +36 -29
  337. udata/tests/dataset/test_dataset_model.py +224 -217
  338. udata/tests/dataset/test_dataset_rdf.py +142 -131
  339. udata/tests/dataset/test_dataset_tasks.py +15 -15
  340. udata/tests/dataset/test_resource_preview.py +10 -13
  341. udata/tests/features/territories/__init__.py +9 -13
  342. udata/tests/features/territories/test_territories_api.py +71 -91
  343. udata/tests/forms/test_basic_fields.py +7 -7
  344. udata/tests/forms/test_current_user_field.py +39 -66
  345. udata/tests/forms/test_daterange_field.py +31 -39
  346. udata/tests/forms/test_dict_field.py +28 -26
  347. udata/tests/forms/test_extras_fields.py +102 -76
  348. udata/tests/forms/test_form_field.py +8 -8
  349. udata/tests/forms/test_image_field.py +33 -26
  350. udata/tests/forms/test_model_field.py +134 -123
  351. udata/tests/forms/test_model_list_field.py +7 -7
  352. udata/tests/forms/test_nested_model_list_field.py +117 -79
  353. udata/tests/forms/test_publish_as_field.py +36 -65
  354. udata/tests/forms/test_reference_field.py +34 -53
  355. udata/tests/forms/test_user_forms.py +23 -21
  356. udata/tests/forms/test_uuid_field.py +6 -10
  357. udata/tests/frontend/__init__.py +9 -6
  358. udata/tests/frontend/test_auth.py +7 -6
  359. udata/tests/frontend/test_csv.py +81 -96
  360. udata/tests/frontend/test_hooks.py +43 -43
  361. udata/tests/frontend/test_markdown.py +211 -191
  362. udata/tests/helpers.py +32 -37
  363. udata/tests/models.py +2 -2
  364. udata/tests/organization/test_csv_adapter.py +21 -16
  365. udata/tests/organization/test_notifications.py +11 -18
  366. udata/tests/organization/test_organization_model.py +13 -13
  367. udata/tests/organization/test_organization_rdf.py +29 -22
  368. udata/tests/organization/test_organization_tasks.py +16 -17
  369. udata/tests/plugin.py +79 -73
  370. udata/tests/reuse/test_reuse_model.py +21 -21
  371. udata/tests/reuse/test_reuse_task.py +11 -13
  372. udata/tests/search/__init__.py +11 -12
  373. udata/tests/search/test_adapter.py +60 -70
  374. udata/tests/search/test_query.py +16 -16
  375. udata/tests/search/test_results.py +10 -7
  376. udata/tests/site/test_site_api.py +11 -16
  377. udata/tests/site/test_site_metrics.py +20 -30
  378. udata/tests/site/test_site_model.py +4 -5
  379. udata/tests/site/test_site_rdf.py +94 -78
  380. udata/tests/test_activity.py +17 -17
  381. udata/tests/test_cors.py +62 -0
  382. udata/tests/test_discussions.py +292 -299
  383. udata/tests/test_i18n.py +37 -40
  384. udata/tests/test_linkchecker.py +91 -85
  385. udata/tests/test_mail.py +13 -17
  386. udata/tests/test_migrations.py +219 -180
  387. udata/tests/test_model.py +164 -157
  388. udata/tests/test_notifications.py +17 -17
  389. udata/tests/test_owned.py +14 -14
  390. udata/tests/test_rdf.py +25 -23
  391. udata/tests/test_routing.py +89 -93
  392. udata/tests/test_storages.py +137 -128
  393. udata/tests/test_tags.py +44 -46
  394. udata/tests/test_topics.py +7 -7
  395. udata/tests/test_transfer.py +42 -49
  396. udata/tests/test_uris.py +160 -161
  397. udata/tests/test_utils.py +79 -71
  398. udata/tests/user/test_user_rdf.py +5 -9
  399. udata/tests/workers/test_jobs_commands.py +57 -58
  400. udata/tests/workers/test_tasks_routing.py +23 -29
  401. udata/tests/workers/test_workers_api.py +125 -131
  402. udata/tests/workers/test_workers_helpers.py +6 -6
  403. udata/tracking.py +4 -6
  404. udata/uris.py +45 -46
  405. udata/utils.py +68 -66
  406. udata/wsgi.py +1 -1
  407. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/METADATA +7 -3
  408. udata-9.1.2.dev30454.dist-info/RECORD +706 -0
  409. udata-9.1.2.dev30355.dist-info/RECORD +0 -704
  410. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/LICENSE +0 -0
  411. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/WHEEL +0 -0
  412. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/entry_points.txt +0 -0
  413. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/top_level.txt +0 -0
udata/api/__init__.py CHANGED
@@ -1,89 +1,53 @@
1
- import itertools
2
1
  import inspect
2
+ import itertools
3
3
  import logging
4
4
  import urllib.parse
5
-
6
5
  from functools import wraps
7
6
  from importlib import import_module
8
7
 
9
8
  from flask import (
10
- current_app, g, request, url_for, json, make_response, redirect, Blueprint
9
+ Blueprint,
10
+ current_app,
11
+ g,
12
+ json,
13
+ make_response,
14
+ redirect,
15
+ request,
16
+ url_for,
11
17
  )
12
- from flask_storage import UnauthorizedFileType
13
18
  from flask_restx import Api, Resource
14
- from flask_cors import CORS
19
+ from flask_storage import UnauthorizedFileType
15
20
 
16
- from udata import tracking, entrypoints
21
+ from udata import cors, entrypoints, tracking
17
22
  from udata.app import csrf
23
+ from udata.auth import Permission, PermissionDenied, RoleNeed, current_user, login_user
18
24
  from udata.i18n import get_locale
19
- from udata.auth import (
20
- current_user, login_user, Permission, RoleNeed, PermissionDenied
21
- )
22
- from udata.utils import safe_unicode
23
25
  from udata.mongo.errors import FieldValidationError
26
+ from udata.utils import safe_unicode
24
27
 
25
28
  from . import fields
26
29
  from .signals import on_api_call
27
30
 
28
-
29
31
  log = logging.getLogger(__name__)
30
32
 
31
- apiv1_blueprint = Blueprint('api', __name__, url_prefix='/api/1')
32
- apiv2_blueprint = Blueprint('apiv2', __name__, url_prefix='/api/2')
33
+ apiv1_blueprint = Blueprint("api", __name__, url_prefix="/api/1")
34
+ apiv2_blueprint = Blueprint("apiv2", __name__, url_prefix="/api/2")
33
35
 
34
36
  DEFAULT_PAGE_SIZE = 50
35
- HEADER_API_KEY = 'X-API-KEY'
36
-
37
- # TODO: make upstream flask-restplus automatically handle
38
- # flask-restplus headers and allow lazy evaluation
39
- # of headers (ie. callable)
40
- PREFLIGHT_HEADERS = (
41
- HEADER_API_KEY,
42
- 'X-Fields',
43
- 'Content-Type',
44
- 'Accept',
45
- 'Accept-Charset',
46
- 'Accept-Language',
47
- 'Authorization',
48
- 'Cache-Control',
49
- 'Content-Encoding',
50
- 'Content-Length',
51
- 'Content-Security-Policy',
52
- 'Content-Type',
53
- 'Cookie',
54
- 'ETag',
55
- 'Host',
56
- 'If-Modified-Since',
57
- 'Keep-Alive',
58
- 'Last-Modified',
59
- 'Origin',
60
- 'Referer',
61
- 'User-Agent',
62
- 'X-Forwarded-For',
63
- 'X-Forwarded-Port',
64
- 'X-Forwarded-Proto',
65
- )
66
-
67
- cors = CORS(allow_headers=PREFLIGHT_HEADERS)
37
+ HEADER_API_KEY = "X-API-KEY"
68
38
 
69
39
 
70
40
  class UDataApi(Api):
71
41
  def __init__(self, app=None, **kwargs):
72
- decorators = kwargs.pop('decorators', []) or []
73
- kwargs['decorators'] = [self.authentify] + decorators
42
+ decorators = kwargs.pop("decorators", []) or []
43
+ kwargs["decorators"] = [self.authentify] + decorators
74
44
  super(UDataApi, self).__init__(app, **kwargs)
75
- self.authorizations = {
76
- 'apikey': {
77
- 'type': 'apiKey',
78
- 'in': 'header',
79
- 'name': HEADER_API_KEY
80
- }
81
- }
45
+ self.authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": HEADER_API_KEY}}
82
46
 
83
47
  def secure(self, func):
84
- '''Enforce authentication on a given method/verb
48
+ """Enforce authentication on a given method/verb
85
49
  and optionally check a given permission
86
- '''
50
+ """
87
51
  if isinstance(func, str):
88
52
  return self._apply_permission(Permission(RoleNeed(func)))
89
53
  elif isinstance(func, Permission):
@@ -94,21 +58,25 @@ class UDataApi(Api):
94
58
  def _apply_permission(self, permission):
95
59
  def wrapper(func):
96
60
  return self._apply_secure(func, permission)
61
+
97
62
  return wrapper
98
63
 
99
64
  def _apply_secure(self, func, permission=None):
100
- '''Enforce authentication on a given method/verb'''
101
- self._build_doc(func, {'security': 'apikey'})
65
+ """Enforce authentication on a given method/verb"""
66
+ self._build_doc(func, {"security": "apikey"})
102
67
 
103
68
  @wraps(func)
104
69
  def wrapper(*args, **kwargs):
105
70
  if (
106
- not current_user.is_anonymous and
107
- not current_user.sysadmin and
108
- current_app.config['READ_ONLY_MODE'] and
109
- any(ext in str(func) for ext in current_app.config['METHOD_BLOCKLIST'])
71
+ not current_user.is_anonymous
72
+ and not current_user.sysadmin
73
+ and current_app.config["READ_ONLY_MODE"]
74
+ and any(ext in str(func) for ext in current_app.config["METHOD_BLOCKLIST"])
110
75
  ):
111
- self.abort(423, 'Due to security reasons, the creation of new content is currently disabled.')
76
+ self.abort(
77
+ 423,
78
+ "Due to security reasons, the creation of new content is currently disabled.",
79
+ )
112
80
 
113
81
  if not current_user.is_authenticated:
114
82
  self.abort(401)
@@ -125,11 +93,12 @@ class UDataApi(Api):
125
93
  return wrapper
126
94
 
127
95
  def authentify(self, func):
128
- '''Authentify the user if credentials are given'''
96
+ """Authentify the user if credentials are given"""
97
+
129
98
  @wraps(func)
130
99
  def wrapper(*args, **kwargs):
131
- from udata.core.user.models import User
132
100
  from udata.api.oauth2 import check_credentials
101
+ from udata.core.user.models import User
133
102
 
134
103
  if current_user.is_authenticated:
135
104
  return func(*args, **kwargs)
@@ -139,72 +108,79 @@ class UDataApi(Api):
139
108
  try:
140
109
  user = User.objects.get(apikey=apikey)
141
110
  except User.DoesNotExist:
142
- self.abort(401, 'Invalid API Key')
111
+ self.abort(401, "Invalid API Key")
143
112
 
144
113
  if not login_user(user, False):
145
- self.abort(401, 'Inactive user')
114
+ self.abort(401, "Inactive user")
146
115
  else:
147
116
  check_credentials()
148
117
  return func(*args, **kwargs)
118
+
149
119
  return wrapper
150
120
 
151
121
  def validate(self, form_cls, obj=None):
152
- '''Validate a form from the request and handle errors'''
153
- if 'application/json' not in request.headers.get('Content-Type', ''):
154
- errors = {'Content-Type': 'expecting application/json'}
122
+ """Validate a form from the request and handle errors"""
123
+ if "application/json" not in request.headers.get("Content-Type", ""):
124
+ errors = {"Content-Type": "expecting application/json"}
155
125
  self.abort(400, errors=errors)
156
- form = form_cls.from_json(request.json, obj=obj, instance=obj,
157
- meta={'csrf': False})
126
+ form = form_cls.from_json(request.json, obj=obj, instance=obj, meta={"csrf": False})
158
127
  if not form.validate():
159
128
  self.abort(400, errors=form.errors)
160
129
  return form
161
130
 
162
131
  def render_ui(self):
163
- return redirect(current_app.config.get('API_DOC_EXTERNAL_LINK'))
132
+ return redirect(current_app.config.get("API_DOC_EXTERNAL_LINK"))
164
133
 
165
134
  def unauthorized(self, response):
166
- '''Override to change the WWW-Authenticate challenge'''
167
- realm = current_app.config.get('HTTP_OAUTH_REALM', 'uData')
135
+ """Override to change the WWW-Authenticate challenge"""
136
+ realm = current_app.config.get("HTTP_OAUTH_REALM", "uData")
168
137
  challenge = 'Bearer realm="{0}"'.format(realm)
169
138
 
170
- response.headers['WWW-Authenticate'] = challenge
139
+ response.headers["WWW-Authenticate"] = challenge
171
140
  return response
172
141
 
173
142
  def page_parser(self):
174
143
  parser = self.parser()
175
- parser.add_argument('page', type=int, default=1, location='args',
176
- help='The page to fetch')
177
- parser.add_argument('page_size', type=int, default=20, location='args',
178
- help='The page size to fetch')
144
+ parser.add_argument("page", type=int, default=1, location="args", help="The page to fetch")
145
+ parser.add_argument(
146
+ "page_size", type=int, default=20, location="args", help="The page size to fetch"
147
+ )
179
148
  return parser
180
149
 
181
150
 
182
151
  api = UDataApi(
183
152
  apiv1_blueprint,
184
153
  decorators=[csrf.exempt],
185
- version='1.0', title='uData API',
186
- description='uData API', default='site',
187
- default_label='Site global namespace'
154
+ version="1.0",
155
+ title="uData API",
156
+ description="uData API",
157
+ default="site",
158
+ default_label="Site global namespace",
188
159
  )
189
160
 
190
161
  apiv2 = UDataApi(
191
162
  apiv2_blueprint,
192
163
  decorators=[csrf.exempt],
193
- version='2.0', title='uData API',
194
- description='udata API v2', default='site',
195
- default_label='Site global namespace',
164
+ version="2.0",
165
+ title="uData API",
166
+ description="udata API v2",
167
+ default="site",
168
+ default_label="Site global namespace",
196
169
  )
197
170
 
198
171
 
199
- api.model_reference = api.model('ModelReference', {
200
- 'class': fields.ClassName(description='The model class', required=True),
201
- 'id': fields.String(description='The object identifier', required=True),
202
- })
172
+ api.model_reference = api.model(
173
+ "ModelReference",
174
+ {
175
+ "class": fields.ClassName(description="The model class", required=True),
176
+ "id": fields.String(description="The object identifier", required=True),
177
+ },
178
+ )
203
179
 
204
180
 
205
- @api.representation('application/json')
181
+ @api.representation("application/json")
206
182
  def output_json(data, code, headers=None):
207
- '''Use Flask JSON to serialize'''
183
+ """Use Flask JSON to serialize"""
208
184
  resp = make_response(json.dumps(data), code)
209
185
  resp.headers.extend(headers or {})
210
186
  return resp
@@ -213,8 +189,8 @@ def output_json(data, code, headers=None):
213
189
  @apiv1_blueprint.before_request
214
190
  @apiv2_blueprint.before_request
215
191
  def set_api_language():
216
- if 'lang' in request.args:
217
- g.lang_code = request.args['lang']
192
+ if "lang" in request.args:
193
+ g.lang_code = request.args["lang"]
218
194
  else:
219
195
  g.lang_code = get_locale()
220
196
 
@@ -225,17 +201,18 @@ def extract_name_from_path(path):
225
201
  Useful to log requests on Piwik with categories tree structure.
226
202
  See: http://piwik.org/faq/how-to/#faq_62
227
203
  """
228
- base_path, query_string = path.split('?')
229
- infos = base_path.strip('/').split('/')[2:] # Removes api/version.
230
- if base_path == '/api/1/' or base_path == '/api/2/': # The API root endpoint redirects to swagger doc.
231
- return safe_unicode('apidoc')
204
+ base_path, query_string = path.split("?")
205
+ infos = base_path.strip("/").split("/")[2:] # Removes api/version.
206
+ if (
207
+ base_path == "/api/1/" or base_path == "/api/2/"
208
+ ): # The API root endpoint redirects to swagger doc.
209
+ return safe_unicode("apidoc")
232
210
  if len(infos) > 1: # This is an object.
233
- name = '{category} / {name}'.format(
234
- category=infos[0].title(),
235
- name=infos[1].replace('-', ' ').title()
211
+ name = "{category} / {name}".format(
212
+ category=infos[0].title(), name=infos[1].replace("-", " ").title()
236
213
  )
237
214
  else: # This is a collection.
238
- name = '{category}'.format(category=infos[0].title())
215
+ name = "{category}".format(category=infos[0].title())
239
216
  return safe_unicode(name)
240
217
 
241
218
 
@@ -243,71 +220,71 @@ def extract_name_from_path(path):
243
220
  @apiv2_blueprint.after_request
244
221
  def collect_stats(response):
245
222
  action_name = extract_name_from_path(request.full_path)
246
- blacklist = current_app.config.get('TRACKING_BLACKLIST', [])
247
- if (not current_app.config['TESTING'] and
248
- request.endpoint not in blacklist):
223
+ blacklist = current_app.config.get("TRACKING_BLACKLIST", [])
224
+ if not current_app.config["TESTING"] and request.endpoint not in blacklist:
249
225
  extras = {
250
- 'action_name': urllib.parse.quote(action_name),
226
+ "action_name": urllib.parse.quote(action_name),
251
227
  }
252
228
  tracking.send_signal(on_api_call, request, current_user, **extras)
253
229
  return response
254
230
 
255
231
 
256
- default_error = api.model('Error', {
257
- 'message': fields.String
258
- })
232
+ default_error = api.model("Error", {"message": fields.String})
259
233
 
260
234
 
261
235
  @api.errorhandler(PermissionDenied)
262
236
  @api.marshal_with(default_error, code=403)
263
237
  def handle_permission_denied(error):
264
- '''Error occuring when the user does not have the required permissions'''
265
- message = 'You do not have the permission to modify that object.'
266
- return {'message': message}, 403
238
+ """Error occuring when the user does not have the required permissions"""
239
+ message = "You do not have the permission to modify that object."
240
+ return {"message": message}, 403
267
241
 
268
242
 
269
243
  @api.errorhandler(ValueError)
270
244
  @api.marshal_with(default_error, code=400)
271
245
  def handle_value_error(error):
272
- '''A generic value error'''
273
- return {'message': str(error)}, 400
246
+ """A generic value error"""
247
+ return {"message": str(error)}, 400
274
248
 
275
249
 
276
250
  @api.errorhandler(UnauthorizedFileType)
277
251
  @api.marshal_with(default_error, code=400)
278
252
  def handle_unauthorized_file_type(error):
279
- '''Error occuring when the user try to upload a non-allowed file type'''
280
- url = url_for('api.allowed_extensions', _external=True)
253
+ """Error occuring when the user try to upload a non-allowed file type"""
254
+ url = url_for("api.allowed_extensions", _external=True)
281
255
  msg = (
282
- 'This file type is not allowed.'
283
- 'The allowed file type list is available at {url}'
256
+ "This file type is not allowed." "The allowed file type list is available at {url}"
284
257
  ).format(url=url)
285
- return {'message': msg}, 400
258
+ return {"message": msg}, 400
259
+
286
260
 
261
+ validation_error_fields = api.model("ValidationError", {"errors": fields.Raw})
287
262
 
288
- validation_error_fields = api.model('ValidationError', {
289
- 'errors': fields.Raw
290
- })
291
263
 
292
264
  @api.errorhandler(FieldValidationError)
293
265
  @api.marshal_with(validation_error_fields, code=400)
294
266
  def handle_validation_error(error: FieldValidationError):
295
- '''A validation error'''
267
+ """A validation error"""
296
268
  errors = {}
297
269
  errors[error.field] = [error.message]
298
270
 
299
- return { 'errors': errors}, 400
271
+ return {"errors": errors}, 400
272
+
300
273
 
301
274
  class API(Resource): # Avoid name collision as resource is a core model
302
275
  pass
303
276
 
304
277
 
305
- base_reference = api.model('BaseReference', {
306
- 'id': fields.String(description='The object unique identifier',
307
- required=True),
308
- 'class': fields.ClassName(description='The object class',
309
- discriminator=True, required=True),
310
- }, description='Base model for reference field, aka. inline model reference')
278
+ base_reference = api.model(
279
+ "BaseReference",
280
+ {
281
+ "id": fields.String(description="The object unique identifier", required=True),
282
+ "class": fields.ClassName(
283
+ description="The object class", discriminator=True, required=True
284
+ ),
285
+ },
286
+ description="Base model for reference field, aka. inline model reference",
287
+ )
311
288
 
312
289
 
313
290
  def marshal_page(page, page_fields):
@@ -340,14 +317,14 @@ def init_app(app):
340
317
  import udata.core.topic.api # noqa
341
318
  import udata.core.topic.apiv2 # noqa
342
319
  import udata.core.post.api # noqa
343
- import udata.core.contact_point.api # noqa
320
+ import udata.core.contact_point.api # noqa
344
321
  import udata.features.transfer.api # noqa
345
322
  import udata.features.notifications.api # noqa
346
323
  import udata.features.identicon.api # noqa
347
324
  import udata.features.territories.api # noqa
348
325
  import udata.harvest.api # noqa
349
326
 
350
- for module in entrypoints.get_enabled('udata.apis', app).values():
327
+ for module in entrypoints.get_enabled("udata.apis", app).values():
351
328
  api_module = module if inspect.ismodule(module) else import_module(module)
352
329
 
353
330
  # api.init_app(app)
@@ -355,5 +332,5 @@ def init_app(app):
355
332
  app.register_blueprint(apiv2_blueprint)
356
333
 
357
334
  from udata.api.oauth2 import init_app as oauth2_init_app
335
+
358
336
  oauth2_init_app(app)
359
- cors.init_app(app)
udata/api/commands.py CHANGED
@@ -2,79 +2,87 @@ import logging
2
2
  import os
3
3
 
4
4
  import click
5
-
6
- from werkzeug.security import gen_salt
7
- from flask import json, current_app
5
+ from flask import current_app, json
8
6
  from flask_restx import schemas
7
+ from werkzeug.security import gen_salt
9
8
 
10
9
  from udata.api import api
11
- from udata.commands import cli, success, exit_with_error
12
- from udata.models import User
13
10
  from udata.api.oauth2 import OAuth2Client
11
+ from udata.commands import cli, exit_with_error, success
12
+ from udata.models import User
14
13
 
15
14
  log = logging.getLogger(__name__)
16
15
 
17
16
 
18
- @cli.group('api')
17
+ @cli.group("api")
19
18
  def grp():
20
- '''API related operations'''
19
+ """API related operations"""
21
20
 
22
21
 
23
22
  def json_to_file(data, filename, pretty=False):
24
- '''Dump JSON data to a file'''
23
+ """Dump JSON data to a file"""
25
24
  kwargs = dict(indent=4) if pretty else {}
26
25
  dirname = os.path.dirname(filename)
27
26
  if not os.path.exists(dirname):
28
27
  os.makedirs(dirname)
29
28
  dump = json.dumps(api.__schema__, **kwargs)
30
- with open(filename, 'wb') as f:
31
- f.write(dump.encode('utf-8'))
29
+ with open(filename, "wb") as f:
30
+ f.write(dump.encode("utf-8"))
32
31
 
33
32
 
34
33
  @grp.command()
35
- @click.argument('filename')
36
- @click.option('-p', '--pretty', is_flag=True, help='Pretty print')
34
+ @click.argument("filename")
35
+ @click.option("-p", "--pretty", is_flag=True, help="Pretty print")
37
36
  def swagger(filename, pretty):
38
- '''Dump the swagger specifications'''
37
+ """Dump the swagger specifications"""
39
38
  json_to_file(api.__schema__, filename, pretty)
40
39
 
41
40
 
42
41
  @grp.command()
43
- @click.argument('filename')
44
- @click.option('-p', '--pretty', is_flag=True, help='Pretty print')
45
- @click.option('-u', '--urlvars', is_flag=True, help='Export query strings')
46
- @click.option('-s', '--swagger', is_flag=True,
47
- help='Export Swagger specifications')
42
+ @click.argument("filename")
43
+ @click.option("-p", "--pretty", is_flag=True, help="Pretty print")
44
+ @click.option("-u", "--urlvars", is_flag=True, help="Export query strings")
45
+ @click.option("-s", "--swagger", is_flag=True, help="Export Swagger specifications")
48
46
  def postman(filename, pretty, urlvars, swagger):
49
- '''Dump the API as a Postman collection'''
47
+ """Dump the API as a Postman collection"""
50
48
  data = api.as_postman(urlvars=urlvars, swagger=swagger)
51
49
  json_to_file(data, filename, pretty)
52
50
 
53
51
 
54
52
  @grp.command()
55
53
  def validate():
56
- '''Validate the Swagger/OpenAPI specification with your config'''
54
+ """Validate the Swagger/OpenAPI specification with your config"""
57
55
  with current_app.test_request_context():
58
56
  schema = json.loads(json.dumps(api.__schema__))
59
57
  try:
60
58
  schemas.validate(schema)
61
- success('API specifications are valid')
59
+ success("API specifications are valid")
62
60
  except schemas.SchemaValidationError as e:
63
- exit_with_error('API specifications are not valid', e)
61
+ exit_with_error("API specifications are not valid", e)
64
62
 
65
63
 
66
64
  @grp.command()
67
- @click.option('-n', '--client-name', default='client-01', help='Client\'s name')
68
- @click.option('-u', '--user-email', help='User\'s email')
69
- @click.option('--uri', multiple=True, default=['http://localhost:8080/login'], help='Client\'s redirect uri')
70
- @click.option('-g', '--grant-types', multiple=True, default=['authorization_code'], help='Client\'s grant types')
71
- @click.option('-s', '--scope', default='default', help='Client\'s scope')
72
- @click.option('-r', '--response-types', multiple=True, default=['code'], help='Client\'s response types')
65
+ @click.option("-n", "--client-name", default="client-01", help="Client's name")
66
+ @click.option("-u", "--user-email", help="User's email")
67
+ @click.option(
68
+ "--uri", multiple=True, default=["http://localhost:8080/login"], help="Client's redirect uri"
69
+ )
70
+ @click.option(
71
+ "-g",
72
+ "--grant-types",
73
+ multiple=True,
74
+ default=["authorization_code"],
75
+ help="Client's grant types",
76
+ )
77
+ @click.option("-s", "--scope", default="default", help="Client's scope")
78
+ @click.option(
79
+ "-r", "--response-types", multiple=True, default=["code"], help="Client's response types"
80
+ )
73
81
  def create_oauth_client(client_name, user_email, uri, grant_types, scope, response_types):
74
- '''Creates an OAuth2Client instance in DB'''
82
+ """Creates an OAuth2Client instance in DB"""
75
83
  user = User.objects(email=user_email).first()
76
84
  if user is None:
77
- exit_with_error('No matching user to email')
85
+ exit_with_error("No matching user to email")
78
86
 
79
87
  client = OAuth2Client.objects.create(
80
88
  name=client_name,
@@ -82,12 +90,12 @@ def create_oauth_client(client_name, user_email, uri, grant_types, scope, respon
82
90
  grant_types=grant_types,
83
91
  scope=scope,
84
92
  response_types=response_types,
85
- redirect_uris=uri
93
+ redirect_uris=uri,
86
94
  )
87
95
 
88
- click.echo(f'New OAuth client: {client.name}')
89
- click.echo(f'Client\'s ID {client.id}')
90
- click.echo(f'Client\'s secret {client.secret}')
91
- click.echo(f'Client\'s grant_types {client.grant_types}')
92
- click.echo(f'Client\'s response_types {client.response_types}')
93
- click.echo(f'Client\'s URI {client.redirect_uris}')
96
+ click.echo(f"New OAuth client: {client.name}")
97
+ click.echo(f"Client's ID {client.id}")
98
+ click.echo(f"Client's secret {client.secret}")
99
+ click.echo(f"Client's grant_types {client.grant_types}")
100
+ click.echo(f"Client's response_types {client.response_types}")
101
+ click.echo(f"Client's URI {client.redirect_uris}")
udata/api/errors.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from udata.i18n import gettext as _
2
2
 
3
-
4
- VALIDATION_ERROR = _('Validation error: your data cannot be updated for '
5
- 'now, we have been notified of the error and we will '
6
- 'fix it as soon as possible.')
3
+ VALIDATION_ERROR = _(
4
+ "Validation error: your data cannot be updated for "
5
+ "now, we have been notified of the error and we will "
6
+ "fix it as soon as possible."
7
+ )