dmart 1.4.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. alembic.ini +117 -0
  2. api/__init__.py +0 -0
  3. api/info/__init__.py +0 -0
  4. api/info/router.py +109 -0
  5. api/managed/__init__.py +0 -0
  6. api/managed/router.py +1541 -0
  7. api/managed/utils.py +1879 -0
  8. api/public/__init__.py +0 -0
  9. api/public/router.py +758 -0
  10. api/qr/__init__.py +0 -0
  11. api/qr/router.py +108 -0
  12. api/user/__init__.py +0 -0
  13. api/user/model/__init__.py +0 -0
  14. api/user/model/errors.py +14 -0
  15. api/user/model/requests.py +165 -0
  16. api/user/model/responses.py +11 -0
  17. api/user/router.py +1413 -0
  18. api/user/service.py +270 -0
  19. bundler.py +55 -0
  20. config/__init__.py +0 -0
  21. config/channels.json +11 -0
  22. config/notification.json +17 -0
  23. cxb/__init__.py +0 -0
  24. cxb/client/__init__.py +0 -0
  25. cxb/client/assets/@codemirror-Rn7_6DkE.js +10 -0
  26. cxb/client/assets/@edraj-CS4NwVbD.js +1 -0
  27. cxb/client/assets/@floating-ui-BwwcF-xh.js +1 -0
  28. cxb/client/assets/@formatjs-yKEsAtjs.js +1 -0
  29. cxb/client/assets/@fortawesome-DRW1UCdr.js +9 -0
  30. cxb/client/assets/@jsonquerylang-laKNoFFq.js +12 -0
  31. cxb/client/assets/@lezer-za4Q-8Ew.js +1 -0
  32. cxb/client/assets/@marijn-DXwl3gUT.js +1 -0
  33. cxb/client/assets/@popperjs-l0sNRNKZ.js +1 -0
  34. cxb/client/assets/@replit--ERk53eB.js +1 -0
  35. cxb/client/assets/@roxi-CGMFK4i8.js +6 -0
  36. cxb/client/assets/@typewriter-cCzskkIv.js +17 -0
  37. cxb/client/assets/@zerodevx-BlBZjKxu.js +1 -0
  38. cxb/client/assets/@zerodevx-CVEpe6WZ.css +1 -0
  39. cxb/client/assets/BreadCrumbLite-DAhOx38v.js +1 -0
  40. cxb/client/assets/EntryRenderer-25YDhRen.js +32 -0
  41. cxb/client/assets/EntryRenderer-DXytdFp9.css +1 -0
  42. cxb/client/assets/ListView-BpAycA2h.js +16 -0
  43. cxb/client/assets/ListView-U8of-_c-.css +1 -0
  44. cxb/client/assets/Prism--hMplq-p.js +3 -0
  45. cxb/client/assets/Prism-Uh6uStUw.css +1 -0
  46. cxb/client/assets/Table2Cols-BsbwicQm.js +1 -0
  47. cxb/client/assets/_..-BvT6vdHa.css +1 -0
  48. cxb/client/assets/_...404_-fuLH_rX9.js +2 -0
  49. cxb/client/assets/_...fallback_-Ba_NLmAE.js +1 -0
  50. cxb/client/assets/_module-Bfk8MiCs.js +3 -0
  51. cxb/client/assets/_module-CEW0D5oI.js +4 -0
  52. cxb/client/assets/_module-Dgq0ZVtz.js +1 -0
  53. cxb/client/assets/ajv-Cpj98o6Y.js +1 -0
  54. cxb/client/assets/axios-CG2WSiiR.js +6 -0
  55. cxb/client/assets/clsx-B-dksMZM.js +1 -0
  56. cxb/client/assets/codemirror-wrapped-line-indent-DPhKvljI.js +1 -0
  57. cxb/client/assets/compare-C3AjiGFR.js +1 -0
  58. cxb/client/assets/compute-scroll-into-view-Bl8rNFhg.js +1 -0
  59. cxb/client/assets/consolite-DlCuI0F9.js +1 -0
  60. cxb/client/assets/crelt-C8TCjufn.js +1 -0
  61. cxb/client/assets/date-fns-l0sNRNKZ.js +1 -0
  62. cxb/client/assets/deepmerge-rn4rBaHU.js +1 -0
  63. cxb/client/assets/dmart_services-AL6-IdDE.js +1 -0
  64. cxb/client/assets/downloadFile-D08i0YDh.js +1 -0
  65. cxb/client/assets/easy-signal-BiPFIK3O.js +1 -0
  66. cxb/client/assets/esm-env-rsSWfq8L.js +1 -0
  67. cxb/client/assets/export-OF_rTiXu.js +1 -0
  68. cxb/client/assets/fast-deep-equal-l0sNRNKZ.js +1 -0
  69. cxb/client/assets/fast-diff-C-IidNf4.js +1 -0
  70. cxb/client/assets/fast-uri-l0sNRNKZ.js +1 -0
  71. cxb/client/assets/flowbite-svelte-BLvjb-sa.js +1 -0
  72. cxb/client/assets/flowbite-svelte-CD54FDqW.css +1 -0
  73. cxb/client/assets/flowbite-svelte-icons-BI8GVhw_.js +1 -0
  74. cxb/client/assets/github-slugger-CQ4oX9Ud.js +1 -0
  75. cxb/client/assets/global-igKv-1g9.js +1 -0
  76. cxb/client/assets/hookar-BMRD9G9H.js +1 -0
  77. cxb/client/assets/immutable-json-patch-DtRO2E_S.js +1 -0
  78. cxb/client/assets/import-1vE3gBat.js +1 -0
  79. cxb/client/assets/index-B-eTh-ZX.js +1 -0
  80. cxb/client/assets/index-BVyxzKtH.js +1 -0
  81. cxb/client/assets/index-BdeNM69f.js +1 -0
  82. cxb/client/assets/index-C6cPO4op.js +1 -0
  83. cxb/client/assets/index-CC-A1ipE.js +1 -0
  84. cxb/client/assets/index-CTxJ-lDp.js +1 -0
  85. cxb/client/assets/index-Cd-F5j_k.js +1 -0
  86. cxb/client/assets/index-D742rwaM.js +1 -0
  87. cxb/client/assets/index-DTfhnhwd.js +1 -0
  88. cxb/client/assets/index-DdXRK7n9.js +2 -0
  89. cxb/client/assets/index-DtiCmB4o.js +1 -0
  90. cxb/client/assets/index-NBrXBlLA.css +2 -0
  91. cxb/client/assets/index-ac-Buu_H.js +4 -0
  92. cxb/client/assets/index-iYkH7C67.js +1 -0
  93. cxb/client/assets/info-B986lRiM.js +1 -0
  94. cxb/client/assets/intl-messageformat-Dc5UU-HB.js +3 -0
  95. cxb/client/assets/jmespath-l0sNRNKZ.js +1 -0
  96. cxb/client/assets/json-schema-traverse-l0sNRNKZ.js +1 -0
  97. cxb/client/assets/json-source-map-DRgZidqy.js +5 -0
  98. cxb/client/assets/jsonpath-plus-l0sNRNKZ.js +1 -0
  99. cxb/client/assets/jsonrepair-B30Dx381.js +8 -0
  100. cxb/client/assets/lodash-es-DZVAA2ox.js +1 -0
  101. cxb/client/assets/marked-DKjyhwJX.js +56 -0
  102. cxb/client/assets/marked-gfm-heading-id-U5zO829x.js +2 -0
  103. cxb/client/assets/marked-mangle-CDMeiHC6.js +1 -0
  104. cxb/client/assets/memoize-one-BdPwpGay.js +1 -0
  105. cxb/client/assets/natural-compare-lite-Bg2Xcf-o.js +7 -0
  106. cxb/client/assets/pagination-svelte-D5CyoiE_.js +13 -0
  107. cxb/client/assets/pagination-svelte-v10nAbbM.css +1 -0
  108. cxb/client/assets/plantuml-encoder-C47mzt9T.js +1 -0
  109. cxb/client/assets/prismjs-DTUiLGJu.js +9 -0
  110. cxb/client/assets/profile-BUf-tKMe.js +1 -0
  111. cxb/client/assets/query-CNmXTsgf.js +1 -0
  112. cxb/client/assets/queryHelpers-C9iBWwqe.js +1 -0
  113. cxb/client/assets/scroll-into-view-if-needed-KR58zyjF.js +1 -0
  114. cxb/client/assets/spaces-0oyGvpii.js +1 -0
  115. cxb/client/assets/style-mod-Bs6eFhZE.js +3 -0
  116. cxb/client/assets/svelte-B2XmcTi_.js +4 -0
  117. cxb/client/assets/svelte-awesome-COLlx0DN.css +1 -0
  118. cxb/client/assets/svelte-awesome-DhnMA6Q_.js +1 -0
  119. cxb/client/assets/svelte-datatables-net-CY7LBj6I.js +1 -0
  120. cxb/client/assets/svelte-floating-ui-BlS3sOAQ.js +1 -0
  121. cxb/client/assets/svelte-i18n-CT2KkQaN.js +3 -0
  122. cxb/client/assets/svelte-jsoneditor-BzfX6Usi.css +1 -0
  123. cxb/client/assets/svelte-jsoneditor-CUGSvWId.js +25 -0
  124. cxb/client/assets/svelte-select-CegQKzqH.css +1 -0
  125. cxb/client/assets/svelte-select-CjHAt_85.js +6 -0
  126. cxb/client/assets/tailwind-merge-CJvxXMcu.js +1 -0
  127. cxb/client/assets/tailwind-variants-Cj20BoQ3.js +1 -0
  128. cxb/client/assets/toast-B9WDyfyI.js +1 -0
  129. cxb/client/assets/tslib-pJfR_DrR.js +1 -0
  130. cxb/client/assets/typewriter-editor-DkTVIJdm.js +25 -0
  131. cxb/client/assets/user-DeK_NB5v.js +1 -0
  132. cxb/client/assets/vanilla-picker-l5rcX3cq.js +8 -0
  133. cxb/client/assets/w3c-keyname-Vcq4gwWv.js +1 -0
  134. cxb/client/config.json +11 -0
  135. cxb/client/config.sample.json +11 -0
  136. cxb/client/favicon.ico +0 -0
  137. cxb/client/favicon.png +0 -0
  138. cxb/client/index.html +28 -0
  139. data_adapters/__init__.py +0 -0
  140. data_adapters/adapter.py +16 -0
  141. data_adapters/base_data_adapter.py +467 -0
  142. data_adapters/file/__init__.py +0 -0
  143. data_adapters/file/adapter.py +2043 -0
  144. data_adapters/file/adapter_helpers.py +1013 -0
  145. data_adapters/file/archive.py +150 -0
  146. data_adapters/file/create_index.py +331 -0
  147. data_adapters/file/create_users_folders.py +52 -0
  148. data_adapters/file/custom_validations.py +68 -0
  149. data_adapters/file/drop_index.py +40 -0
  150. data_adapters/file/health_check.py +560 -0
  151. data_adapters/file/redis_services.py +1110 -0
  152. data_adapters/helpers.py +27 -0
  153. data_adapters/sql/__init__.py +0 -0
  154. data_adapters/sql/adapter.py +3218 -0
  155. data_adapters/sql/adapter_helpers.py +491 -0
  156. data_adapters/sql/create_tables.py +451 -0
  157. data_adapters/sql/create_users_folders.py +53 -0
  158. data_adapters/sql/db_to_json_migration.py +485 -0
  159. data_adapters/sql/health_check_sql.py +232 -0
  160. data_adapters/sql/json_to_db_migration.py +454 -0
  161. data_adapters/sql/update_query_policies.py +101 -0
  162. data_generator.py +81 -0
  163. dmart-1.4.17.dist-info/METADATA +65 -0
  164. dmart-1.4.17.dist-info/RECORD +289 -0
  165. dmart-1.4.17.dist-info/WHEEL +5 -0
  166. dmart-1.4.17.dist-info/entry_points.txt +2 -0
  167. dmart-1.4.17.dist-info/top_level.txt +24 -0
  168. dmart.py +623 -0
  169. dmart_migrations/README +1 -0
  170. dmart_migrations/__init__.py +0 -0
  171. dmart_migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  172. dmart_migrations/__pycache__/env.cpython-314.pyc +0 -0
  173. dmart_migrations/env.py +100 -0
  174. dmart_migrations/notes.txt +11 -0
  175. dmart_migrations/script.py.mako +28 -0
  176. dmart_migrations/scripts/__init__.py +0 -0
  177. dmart_migrations/scripts/calculate_checksums.py +77 -0
  178. dmart_migrations/scripts/migration_f7a4949eed19.py +28 -0
  179. dmart_migrations/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
  180. dmart_migrations/versions/10d2041b94d4_last_checksum_history.py +62 -0
  181. dmart_migrations/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
  182. dmart_migrations/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
  183. dmart_migrations/versions/3c8bca2219cc_add_otp_table.py +38 -0
  184. dmart_migrations/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
  185. dmart_migrations/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
  186. dmart_migrations/versions/74288ccbd3b5_initial.py +264 -0
  187. dmart_migrations/versions/7520a89a8467_rm_activesession_table.py +39 -0
  188. dmart_migrations/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
  189. dmart_migrations/versions/8640dcbebf85_add_notes_to_users.py +32 -0
  190. dmart_migrations/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
  191. dmart_migrations/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
  192. dmart_migrations/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
  193. dmart_migrations/versions/__init__.py +0 -0
  194. dmart_migrations/versions/__pycache__/0f3d2b1a7c21_add_authz_materialized_views.cpython-314.pyc +0 -0
  195. dmart_migrations/versions/__pycache__/10d2041b94d4_last_checksum_history.cpython-314.pyc +0 -0
  196. dmart_migrations/versions/__pycache__/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.cpython-314.pyc +0 -0
  197. dmart_migrations/versions/__pycache__/26bfe19b49d4_rm_failedloginattempts.cpython-314.pyc +0 -0
  198. dmart_migrations/versions/__pycache__/3c8bca2219cc_add_otp_table.cpython-314.pyc +0 -0
  199. dmart_migrations/versions/__pycache__/6675fd9dfe42_remove_unique_from_sessions_table.cpython-314.pyc +0 -0
  200. dmart_migrations/versions/__pycache__/71bc1df82e6a_adding_user_last_login_at.cpython-314.pyc +0 -0
  201. dmart_migrations/versions/__pycache__/74288ccbd3b5_initial.cpython-314.pyc +0 -0
  202. dmart_migrations/versions/__pycache__/7520a89a8467_rm_activesession_table.cpython-314.pyc +0 -0
  203. dmart_migrations/versions/__pycache__/848b623755a4_make_created_nd_updated_at_required.cpython-314.pyc +0 -0
  204. dmart_migrations/versions/__pycache__/8640dcbebf85_add_notes_to_users.cpython-314.pyc +0 -0
  205. dmart_migrations/versions/__pycache__/91c94250232a_adding_fk_on_owner_shortname.cpython-314.pyc +0 -0
  206. dmart_migrations/versions/__pycache__/98ecd6f56f9a_ext_meta_with_owner_group_shortname.cpython-314.pyc +0 -0
  207. dmart_migrations/versions/__pycache__/9aae9138c4ef_indexing_created_at_updated_at.cpython-314.pyc +0 -0
  208. dmart_migrations/versions/__pycache__/b53f916b3f6d_json_to_jsonb.cpython-314.pyc +0 -0
  209. dmart_migrations/versions/__pycache__/eb5f1ec65156_adding_user_locked_to_device.cpython-314.pyc +0 -0
  210. dmart_migrations/versions/__pycache__/f7a4949eed19_adding_query_policies_to_meta.cpython-314.pyc +0 -0
  211. dmart_migrations/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
  212. dmart_migrations/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
  213. dmart_migrations/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
  214. get_settings.py +7 -0
  215. info.json +1 -0
  216. languages/__init__.py +0 -0
  217. languages/arabic.json +15 -0
  218. languages/english.json +16 -0
  219. languages/kurdish.json +14 -0
  220. languages/loader.py +12 -0
  221. main.py +560 -0
  222. migrate.py +24 -0
  223. models/__init__.py +0 -0
  224. models/api.py +203 -0
  225. models/core.py +597 -0
  226. models/enums.py +255 -0
  227. password_gen.py +8 -0
  228. plugins/__init__.py +0 -0
  229. plugins/action_log/__init__.py +0 -0
  230. plugins/action_log/plugin.py +121 -0
  231. plugins/admin_notification_sender/__init__.py +0 -0
  232. plugins/admin_notification_sender/plugin.py +124 -0
  233. plugins/ldap_manager/__init__.py +0 -0
  234. plugins/ldap_manager/plugin.py +100 -0
  235. plugins/local_notification/__init__.py +0 -0
  236. plugins/local_notification/plugin.py +123 -0
  237. plugins/realtime_updates_notifier/__init__.py +0 -0
  238. plugins/realtime_updates_notifier/plugin.py +58 -0
  239. plugins/redis_db_update/__init__.py +0 -0
  240. plugins/redis_db_update/plugin.py +188 -0
  241. plugins/resource_folders_creation/__init__.py +0 -0
  242. plugins/resource_folders_creation/plugin.py +81 -0
  243. plugins/system_notification_sender/__init__.py +0 -0
  244. plugins/system_notification_sender/plugin.py +188 -0
  245. plugins/update_access_controls/__init__.py +0 -0
  246. plugins/update_access_controls/plugin.py +9 -0
  247. pytests/__init__.py +0 -0
  248. pytests/api_user_models_erros_test.py +16 -0
  249. pytests/api_user_models_requests_test.py +98 -0
  250. pytests/archive_test.py +72 -0
  251. pytests/base_test.py +300 -0
  252. pytests/get_settings_test.py +14 -0
  253. pytests/json_to_db_migration_test.py +237 -0
  254. pytests/service_test.py +26 -0
  255. pytests/test_info.py +55 -0
  256. pytests/test_status.py +15 -0
  257. run_notification_campaign.py +85 -0
  258. scheduled_notification_handler.py +121 -0
  259. schema_migration.py +208 -0
  260. schema_modulate.py +192 -0
  261. set_admin_passwd.py +55 -0
  262. sync.py +202 -0
  263. utils/__init__.py +0 -0
  264. utils/access_control.py +306 -0
  265. utils/async_request.py +8 -0
  266. utils/exporter.py +309 -0
  267. utils/firebase_notifier.py +57 -0
  268. utils/generate_email.py +37 -0
  269. utils/helpers.py +352 -0
  270. utils/hypercorn_config.py +12 -0
  271. utils/internal_error_code.py +60 -0
  272. utils/jwt.py +124 -0
  273. utils/logger.py +167 -0
  274. utils/middleware.py +99 -0
  275. utils/notification.py +75 -0
  276. utils/password_hashing.py +16 -0
  277. utils/plugin_manager.py +202 -0
  278. utils/query_policies_helper.py +128 -0
  279. utils/regex.py +44 -0
  280. utils/repository.py +529 -0
  281. utils/router_helper.py +19 -0
  282. utils/settings.py +166 -0
  283. utils/sms_notifier.py +21 -0
  284. utils/social_sso.py +67 -0
  285. utils/templates/activation.html.j2 +26 -0
  286. utils/templates/reminder.html.j2 +17 -0
  287. utils/ticket_sys_utils.py +203 -0
  288. utils/web_notifier.py +29 -0
  289. websocket.py +231 -0
utils/regex.py ADDED
@@ -0,0 +1,44 @@
1
+ import re
2
+
3
+ SUBPATH = "^[a-zA-Z\u0621-\u064A0-9\u0660-\u0669\u064B-\u065F_/]{1,128}$"
4
+ SHORTNAME = "^[a-zA-Z\u0621-\u064A0-9\u0660-\u0669\u064B-\u065F_]{1,64}$"
5
+ SLUG = "^[a-zA-Z0-9_-]{1,64}$"
6
+ FILENAME = "^[a-zA-Z\u0621-\u064A0-9\u0660-\u0669\u064B-\u065F_]{1,32}\\.(gif|png|jpeg|jpg|pdf|wsq|mp3|mp4|csv|jsonl|parquet|sqlite|sqlite3|sqlite|db|duckdb|svg|apk)$"
7
+ SPACENAME = "^[a-zA-Z\u0621-\u064A0-9\u0660-\u0669\u064B-\u065F_]{1,32}$"
8
+ EXT = "^(gif|png|jpeg|jpg|webp|json|md|pdf|wsq|mp3|mp4|csv|jsonl|parquet|sqlite|sqlite3|db|db3|s3db|sl3|duckdb|svg|xsls|docx|apk)$"
9
+ IMG_EXT = "^(gif|png|jpeg|jpg|wsq|svg|webp)$"
10
+ USERNAME = "^[a-zA-Z\u0621-\u064A0-9\u0660-\u0669\u064B-\u065F_]{3,10}$"
11
+ PASSWORD = "^(?=.*[0-9\u0660-\u0669])(?=.*[A-Z\u0621-\u064A])[a-zA-Z\u0621-\u064A0-9\u0660-\u0669_#@%*!?$^-]{8,24}$"
12
+ EMAIL = (
13
+ "^[a-zA-Z\u0621-\u064A0-9\u0660-\u0669_\\.-]+@([a-zA-Z\u0621-\u064A0-9\u0660-\u0669_-]+\\.)+"
14
+ "[a-zA-Z\u0621-\u064A0-9\u0660-\u0669_-]{2,4}$"
15
+ )
16
+ META_DOC_ID = (
17
+ "^[a-zA-Z\u0621-\u064A0-9\u0660-\u0669_]*:[a-zA-Z\u0621-\u064A0-9\u0660-\u0669_]"
18
+ "[a-zA-Z\u0621-\u064A0-9\u0660-\u0669_]*:meta:[a-zA-Z\u0621-\u064A0-9\u0660-\u0669_/]+$"
19
+ )
20
+ MSISDN = "^[1-9][0-9]{9, 14}$" # Between 10 and 14 digits, not starting with zero
21
+ EXTENDED_MSISDN = (
22
+ "^[1-9][0-9]{9,14}$" # Between 10 and 14 digits, not starting with zero
23
+ )
24
+ OTP_CODE = "^[0-9\u0660-\u0669]{6}$" # Exactly 6 digits
25
+ INVITATION = (
26
+ "^([a-zA-Z\u0621-\u064A0-9\u0660-\u0669_=]+)\\.([a-zA-Z\u0621-\u064A0-9\u0660-\u0669_=]+)"
27
+ "\\.([a-zA-Z\u0621-\u064A0-9\u0660-\u0669_+/=-]*)$"
28
+ )
29
+
30
+ FILE_PATTERN = re.compile(
31
+ "\\.dm/([a-zA-Z\u0621-\u064A0-9\u0660-\u0669_]*)/meta\\.([a-zA-Z\u0621-\u064A]*)\\.json$"
32
+ )
33
+ PAYLOAD_FILE_PATTERN = re.compile("([a-zA-Z\u0621-\u064A0-9\u0660-\u0669_]*)\\.json$")
34
+ # HISTORY_PATTERN = re.compile("([0-9\u0660-\u0669]*)\\.json$")
35
+ ATTACHMENT_PATTERN = re.compile(
36
+ "attachments\\.*[a-zA-Z\u0621-\u064A0-9\u0660-\u0669_]*\\.([a-zA-Z\u0621-\u064A0-9\u0660-\u0669_]+)"
37
+ "/meta\\.([a-zA-Z\u0621-\u064A0-9\u0660-\u0669_]*)\\.json$"
38
+ )
39
+ FOLDER_PATTERN = re.compile(
40
+ "/([a-zA-Z\u0621-\u064A0-9\u0660-\u0669\u064B-\u065F_]*)/\\.dm/meta\\.folder\\.json$"
41
+ )
42
+ SPACES_PATTERN = re.compile(
43
+ "/([a-zA-Z\u0621-\u064A0-9\u0660-\u0669\u064B-\u065F_]*)/\\.dm/meta\\.space\\.json$"
44
+ )
utils/repository.py ADDED
@@ -0,0 +1,529 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import asyncio
5
+ import json
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from uuid import uuid4
10
+ from fastapi import status
11
+ import models.api as api
12
+ import models.core as core
13
+ import utils.regex as regex
14
+ from models.enums import ContentType, Language
15
+ from data_adapters.adapter import data_adapter as db
16
+ from utils.helpers import (
17
+ camel_case,
18
+ jq_dict_parser,
19
+ )
20
+ from utils.internal_error_code import InternalErrorCode
21
+ from utils.jwt import generate_jwt
22
+ from utils.settings import settings
23
+
24
+ async def serve_query(
25
+ query: api.Query, logged_in_user: str
26
+ ) -> tuple[int, list[core.Record]]:
27
+ records: list[core.Record] = []
28
+ total: int = 0
29
+
30
+ total, records = await db.query(query, logged_in_user)
31
+
32
+ try:
33
+ for _r in records or []:
34
+ attrs = getattr(_r, "attributes", None)
35
+ if isinstance(attrs, dict):
36
+ attrs.pop("password", None)
37
+ except Exception:
38
+ pass
39
+
40
+ if query.jq_filter:
41
+ try:
42
+ def _run_jq_subprocess() -> list:
43
+ _input_local = [record.model_dump() for record in records]
44
+ _input_local = jq_dict_parser(_input_local)
45
+ input_json = json.dumps(_input_local)
46
+
47
+ cmd = ["jq", "-c", query.jq_filter]
48
+
49
+ try:
50
+ completed = subprocess.run(
51
+ cmd, # type: ignore
52
+ input=input_json.encode("utf-8"),
53
+ stdout=subprocess.PIPE,
54
+ stderr=subprocess.PIPE,
55
+ timeout=settings.jq_timeout,
56
+ check=False,
57
+ )
58
+ except subprocess.TimeoutExpired:
59
+ raise api.Exception(
60
+ status.HTTP_400_BAD_REQUEST,
61
+ api.Error(
62
+ type="request",
63
+ code=InternalErrorCode.JQ_TIMEOUT,
64
+ message="jq filter took too long to execute",
65
+ ),
66
+ )
67
+
68
+ if completed.returncode != 0:
69
+ raise api.Exception(
70
+ status.HTTP_400_BAD_REQUEST,
71
+ api.Error(
72
+ type="request",
73
+ code=InternalErrorCode.JQ_ERROR,
74
+ message="jq filter failed to be executed",
75
+ ),
76
+ )
77
+
78
+ stdout = completed.stdout.decode("utf-8")
79
+ results: list = []
80
+ if stdout.startswith("[") and stdout.endswith("]\n"):
81
+ results = json.loads(stdout)
82
+ else:
83
+ for line in stdout.splitlines():
84
+ line = line.strip()
85
+ if not line:
86
+ continue
87
+ results.append(json.loads(line))
88
+ return results
89
+
90
+ loop = asyncio.get_running_loop()
91
+ records = await asyncio.wait_for(
92
+ loop.run_in_executor(None, _run_jq_subprocess),
93
+ timeout=settings.jq_timeout,
94
+ )
95
+
96
+ except FileNotFoundError:
97
+ raise api.Exception(
98
+ status.HTTP_400_BAD_REQUEST,
99
+ api.Error(
100
+ type="request",
101
+ code=InternalErrorCode.NOT_ALLOWED,
102
+ message="jq is not installed!",
103
+ ),
104
+ )
105
+
106
+ return total, records
107
+
108
+
109
+ async def get_last_updated_entry(
110
+ space_name: str,
111
+ schema_names: list,
112
+ retrieve_json_payload: bool,
113
+ logged_in_user: str,
114
+ ):
115
+ report_query = api.Query(
116
+ type=api.QueryType.search,
117
+ space_name=space_name,
118
+ subpath="/",
119
+ search=f"@schema_shortname:{'|'.join(schema_names)}",
120
+ filter_schema_names=["meta"],
121
+ sort_by="updated_at",
122
+ sort_type=api.SortType.descending,
123
+ limit=50, # to be in safe side if the query filtered out some invalid entries
124
+ retrieve_json_payload=retrieve_json_payload,
125
+ )
126
+ _, records = await serve_query(report_query, logged_in_user)
127
+
128
+ return records[0] if records else None
129
+
130
+
131
+ async def get_resource_obj_or_none(
132
+ *,
133
+ space_name: str,
134
+ subpath: str,
135
+ shortname: str,
136
+ resource_type: str,
137
+ user_shortname: str,
138
+ ):
139
+ resource_cls = getattr(
140
+ sys.modules["models.core"], camel_case(resource_type))
141
+ try:
142
+ return await db.load(
143
+ space_name=space_name,
144
+ subpath=subpath,
145
+ shortname=shortname,
146
+ class_type=resource_cls,
147
+ user_shortname=user_shortname,
148
+ )
149
+ except Exception:
150
+ return None
151
+
152
+
153
+ async def get_payload_obj_or_none(
154
+ *,
155
+ space_name: str,
156
+ subpath: str,
157
+ filename: str,
158
+ resource_type: str,
159
+ ):
160
+ resource_cls = getattr(
161
+ sys.modules["models.core"], camel_case(resource_type))
162
+ try:
163
+ return await db.load_resource_payload(
164
+ space_name=space_name,
165
+ subpath=subpath,
166
+ filename=filename,
167
+ class_type=resource_cls,
168
+ )
169
+ except Exception:
170
+ return None
171
+
172
+
173
+ async def folder_meta_content_check(
174
+ space_name, subpath, folder_name, spaces_path_parts,
175
+ user_shortname, folder_name_index, invalid_folders
176
+ ):
177
+ try:
178
+ folder_meta_content = await db.load(
179
+ space_name=space_name,
180
+ subpath=folder_name,
181
+ shortname="",
182
+ class_type=core.Folder,
183
+ user_shortname=user_shortname,
184
+ )
185
+ if (
186
+ folder_meta_content.payload
187
+ and folder_meta_content.payload.content_type == ContentType.json
188
+ ):
189
+ payload_path = "/"
190
+ subpath_parts = subpath.split("/")
191
+ if len(subpath_parts) > (len(spaces_path_parts) + 2):
192
+ payload_path = "/".join(
193
+ subpath_parts[folder_name_index:-1])
194
+ folder_meta_payload = await db.load_resource_payload(
195
+ space_name,
196
+ payload_path,
197
+ str(folder_meta_content.payload.body),
198
+ core.Folder,
199
+ )
200
+ if folder_meta_content.payload.schema_shortname and folder_meta_payload:
201
+ await db.validate_payload_with_schema(
202
+ payload_data=folder_meta_payload,
203
+ space_name=space_name,
204
+ schema_shortname=folder_meta_content.payload.schema_shortname,
205
+ )
206
+ except Exception:
207
+ invalid_folders.append(folder_name)
208
+
209
+
210
+ async def health_check_entry_vsd(
211
+ space_name, folder_name, entry_shortname, entry_resource_type, user_shortname,
212
+ folder, folders_report, max_invalid_size, entry_meta_obj
213
+ ):
214
+ await health_check_entry(
215
+ space_name=space_name,
216
+ subpath=folder_name,
217
+ shortname=entry_shortname,
218
+ resource_type=entry_resource_type,
219
+ user_shortname=user_shortname,
220
+ )
221
+
222
+ # VALIDATE ENTRY ATTACHMENTS
223
+ attachments_path = f"{folder.path}/{entry_shortname}"
224
+ attachment_folders = os.scandir(attachments_path)
225
+ for attachment_folder in attachment_folders:
226
+ # i.e. attachment_folder = attachments.media
227
+ if attachment_folder.is_file():
228
+ continue
229
+
230
+ attachment_folder_files = os.scandir(attachment_folder)
231
+ for attachment_folder_file in attachment_folder_files:
232
+ # i.e. attachment_folder_file = meta.*.json or *.png
233
+ if (
234
+ not attachment_folder_file.is_file()
235
+ or not os.access(attachment_folder_file.path, os.W_OK)
236
+ or not os.access(attachment_folder_file.path, os.R_OK)
237
+ ):
238
+ raise Exception(
239
+ f"can't access this attachment {attachment_folder_file.path[len(str(settings.spaces_folder)):]}"
240
+ )
241
+
242
+ attachment_match = regex.ATTACHMENT_PATTERN.search(attachment_folder_file.path)
243
+ if not attachment_match:
244
+ # if it's the media file not its meta json file
245
+ continue
246
+ attachment_shortname = attachment_match.group(2)
247
+ attachment_resource_type = attachment_match.group(1)
248
+ await health_check_entry(
249
+ space_name=space_name,
250
+ subpath=f"{folder_name}/{entry_shortname}",
251
+ shortname=attachment_shortname,
252
+ resource_type=attachment_resource_type,
253
+ user_shortname=user_shortname,
254
+ )
255
+
256
+ if "valid_entries" not in folders_report[folder_name]:
257
+ folders_report[folder_name]["valid_entries"] = 1
258
+ else:
259
+ folders_report[folder_name]["valid_entries"] += 1
260
+
261
+
262
+ async def validate_subpath_data(
263
+ space_name: str,
264
+ subpath: str,
265
+ user_shortname: str,
266
+ invalid_folders: list[str],
267
+ folders_report: dict[str, dict[str, Any]],
268
+ meta_folders_health: list[str],
269
+ max_invalid_size: int,
270
+ ):
271
+ """
272
+ Params:
273
+ @subpath: str holding the full path, ex: ../spaces/aftersales/reports
274
+
275
+ Algorithm:
276
+ - if subpath ends with .dm return
277
+ - for folder in scandir(subpath)
278
+ - if folder ends with .dm
279
+ - get folder_meta = folder/meta.folder.json
280
+ - validate folder_meta
281
+ - loop over folder.entries and validate them along with theire attachments
282
+ - else
283
+ - call myself with subpath = folder
284
+ """
285
+ spaces_path_parts = str(settings.spaces_folder).split("/")
286
+ folder_name_index = len(spaces_path_parts) + 1
287
+
288
+ if subpath.endswith(".dm"):
289
+ return
290
+
291
+ subpath_folders = os.scandir(subpath)
292
+ for folder in subpath_folders:
293
+ if not folder.is_dir():
294
+ continue
295
+
296
+ if folder.name != ".dm":
297
+ await validate_subpath_data(
298
+ space_name,
299
+ folder.path,
300
+ user_shortname,
301
+ invalid_folders,
302
+ folders_report,
303
+ meta_folders_health,
304
+ max_invalid_size,
305
+ )
306
+ continue
307
+
308
+ folder_meta = Path(f"{folder.path}/meta.folder.json")
309
+ folder_name = "/".join(subpath.split("/")[folder_name_index:])
310
+ if not folder_meta.is_file():
311
+ meta_folders_health.append(
312
+ str(folder_meta)[len(str(settings.spaces_folder)):]
313
+ )
314
+ continue
315
+
316
+ await folder_meta_content_check(
317
+ space_name, subpath, folder_name, spaces_path_parts,
318
+ user_shortname, folder_name_index, invalid_folders
319
+ )
320
+
321
+ folders_report.setdefault(folder_name, {})
322
+
323
+ # VALIDATE FOLDER ENTRIES
324
+ folder_entries = os.scandir(folder.path)
325
+ for entry in folder_entries:
326
+ if entry.is_file():
327
+ continue
328
+
329
+ entry_files = os.scandir(entry)
330
+ entry_match = None
331
+ for file in entry_files:
332
+ if file.is_dir():
333
+ continue
334
+ entry_match = regex.FILE_PATTERN.search(file.path)
335
+
336
+ if entry_match:
337
+ break
338
+
339
+ if not entry_match:
340
+ issue = {
341
+ "issues": ["meta"],
342
+ "uuid": "",
343
+ "shortname": entry.name,
344
+ "exception": f"Can't access this meta {subpath[len(str(settings.spaces_folder)):]}/{entry.name}",
345
+ }
346
+
347
+ if "invalid_entries" not in folders_report[folder_name]:
348
+ folders_report[folder_name]["invalid_entries"] = [issue]
349
+ else:
350
+ if (
351
+ len(folders_report[folder_name]["invalid_entries"])
352
+ >= max_invalid_size
353
+ ):
354
+ break
355
+ folders_report[folder_name]["invalid_entries"].append(
356
+ issue)
357
+ continue
358
+
359
+ entry_shortname = entry_match.group(1)
360
+ entry_resource_type = entry_match.group(2)
361
+
362
+ if folder_name == "schema" and entry_shortname == "meta_schema":
363
+ folders_report[folder_name].setdefault("valid_entries", 0)
364
+ folders_report[folder_name]["valid_entries"] += 1
365
+ continue
366
+
367
+ entry_meta_obj = None
368
+ try:
369
+ await health_check_entry_vsd(
370
+ space_name, folder_name, entry_shortname, entry_resource_type, user_shortname,
371
+ folder, folders_report, max_invalid_size, entry_meta_obj
372
+ )
373
+ except Exception as e:
374
+ issue_type = "payload"
375
+ uuid = ""
376
+ if not entry_meta_obj:
377
+ issue_type = "meta"
378
+ else:
379
+ uuid = str(
380
+ entry_meta_obj.uuid) if entry_meta_obj.uuid else ""
381
+
382
+ issue = {
383
+ "issues": [issue_type],
384
+ "uuid": uuid,
385
+ "shortname": entry_shortname,
386
+ "resource_type": entry_resource_type,
387
+ "exception": str(e),
388
+ }
389
+
390
+ if "invalid_entries" not in folders_report[folder_name]:
391
+ folders_report[folder_name]["invalid_entries"] = [issue]
392
+ else:
393
+ if (
394
+ len(folders_report[folder_name]["invalid_entries"])
395
+ >= max_invalid_size
396
+ ):
397
+ break
398
+ folders_report[folder_name]["invalid_entries"].append(
399
+ issue
400
+ )
401
+
402
+ if not folders_report.get(folder_name, {}):
403
+ del folders_report[folder_name]
404
+
405
+
406
+ async def health_check_entry(
407
+ space_name: str,
408
+ subpath: str,
409
+ resource_type: str,
410
+ shortname: str,
411
+ user_shortname: str,
412
+ ):
413
+ resource_class = getattr(
414
+ sys.modules["models.core"], camel_case(resource_type)
415
+ )
416
+ entry_meta_obj = resource_class.model_validate(await db.load(
417
+ space_name=space_name,
418
+ subpath=subpath,
419
+ shortname=shortname,
420
+ class_type=resource_class,
421
+ user_shortname=user_shortname,
422
+ ))
423
+ if entry_meta_obj.shortname != shortname:
424
+ raise Exception(
425
+ "the shortname which got from the folder path doesn't match the shortname in the meta file."
426
+ )
427
+ payload_file_path = None
428
+ if (
429
+ entry_meta_obj.payload
430
+ and entry_meta_obj.payload.content_type == ContentType.image
431
+ ):
432
+ payload_file_path = Path(f"{subpath}/{entry_meta_obj.payload.body}")
433
+ if (
434
+ not payload_file_path.is_file()
435
+ or not bool(
436
+ re.match(
437
+ regex.IMG_EXT,
438
+ entry_meta_obj.payload.body.split(".")[-1],
439
+ )
440
+ )
441
+ or not os.access(payload_file_path, os.R_OK)
442
+ or not os.access(payload_file_path, os.W_OK)
443
+ ):
444
+ if payload_file_path:
445
+ raise Exception(
446
+ f"can't access this payload {payload_file_path}"
447
+ )
448
+ else:
449
+ raise Exception(
450
+ f"can't access this payload {subpath}"
451
+ f"/{entry_meta_obj.shortname}"
452
+ )
453
+ elif (
454
+ entry_meta_obj.payload
455
+ and isinstance(entry_meta_obj.payload.body, str)
456
+ and entry_meta_obj.payload.content_type == ContentType.json
457
+ ):
458
+ payload_file_path = db.payload_path(space_name, subpath, resource_class)
459
+ if not entry_meta_obj.payload.body.endswith(
460
+ ".json"
461
+ ) or not os.access(payload_file_path, os.W_OK):
462
+ raise Exception(
463
+ f"can't access this payload {payload_file_path}"
464
+ )
465
+ payload_file_content = await db.load_resource_payload(
466
+ space_name,
467
+ subpath,
468
+ entry_meta_obj.payload.body,
469
+ resource_class,
470
+ )
471
+ if entry_meta_obj.payload.schema_shortname and payload_file_content:
472
+ await db.validate_payload_with_schema(
473
+ payload_data=payload_file_content,
474
+ space_name=space_name,
475
+ schema_shortname=entry_meta_obj.payload.schema_shortname,
476
+ )
477
+
478
+ if (
479
+ entry_meta_obj.payload.checksum and
480
+ entry_meta_obj.payload.client_checksum and
481
+ entry_meta_obj.payload.checksum != entry_meta_obj.payload.client_checksum
482
+ ):
483
+ raise Exception(
484
+ f"payload.checksum not equal payload.client_checksum {subpath}/{entry_meta_obj.shortname}"
485
+ )
486
+
487
+ async def url_shortner(url: str) -> str:
488
+ token_uuid = str(uuid4())[:8]
489
+ await db.set_url_shortner(token_uuid, url)
490
+ return f"{settings.public_app_url}/managed/s/{token_uuid}"
491
+
492
+
493
+ async def store_user_invitation_token(user: core.User, channel: str) -> str | None:
494
+ """Generate and Store an invitation token
495
+
496
+ Returns:
497
+ invitation link or None if the user is not eligible
498
+ """
499
+ invitation_value = None
500
+ if channel == "SMS" and user.msisdn:
501
+ invitation_value = f"{channel}:{user.msisdn}"
502
+ elif channel == "EMAIL" and user.email:
503
+ invitation_value = f"{channel}:{user.email}"
504
+
505
+ if not invitation_value:
506
+ return None
507
+
508
+ invitation_token = generate_jwt(
509
+ {"shortname": user.shortname, "channel": channel},
510
+ settings.jwt_access_expires,
511
+ )
512
+
513
+ await db.set_invitation(invitation_token, invitation_value)
514
+
515
+ return core.User.invitation_url_template() \
516
+ .replace("{url}", settings.invitation_link) \
517
+ .replace("{token}", invitation_token) \
518
+ .replace("{lang}", Language.code(user.language)) \
519
+ .replace("{user_type}", user.type)
520
+
521
+
522
+ async def delete_space(space_name, record, owner_shortname):
523
+ if settings.active_data_db == "sql":
524
+ resource_obj = core.Meta.from_record(
525
+ record=record, owner_shortname=owner_shortname
526
+ )
527
+ await db.delete(space_name, record.subpath, resource_obj, owner_shortname)
528
+
529
+ os.system(f"rm -r {settings.spaces_folder}/{space_name}")
utils/router_helper.py ADDED
@@ -0,0 +1,19 @@
1
+ from utils.internal_error_code import InternalErrorCode
2
+ from data_adapters.adapter import data_adapter as db
3
+ import models.api as api
4
+ from fastapi import status
5
+
6
+
7
+ async def is_space_exist(space_name, should_exist=True):
8
+
9
+ space = await db.fetch_space(space_name)
10
+
11
+ if (space is not None) ^ should_exist:
12
+ raise api.Exception(
13
+ status.HTTP_400_BAD_REQUEST,
14
+ api.Error(
15
+ type="request",
16
+ code=InternalErrorCode.INVALID_SPACE_NAME,
17
+ message=f"Space name {space_name} provided is empty or invalid [3]",
18
+ ),
19
+ )