dmart 0.1.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.
Files changed (106) hide show
  1. alembic/__init__.py +0 -0
  2. alembic/env.py +91 -0
  3. api/__init__.py +0 -0
  4. api/info/__init__.py +0 -0
  5. api/info/router.py +109 -0
  6. api/managed/__init__.py +0 -0
  7. api/managed/router.py +1541 -0
  8. api/managed/utils.py +1850 -0
  9. api/public/__init__.py +0 -0
  10. api/public/router.py +758 -0
  11. api/qr/__init__.py +0 -0
  12. api/qr/router.py +108 -0
  13. api/user/__init__.py +0 -0
  14. api/user/router.py +1401 -0
  15. api/user/service.py +270 -0
  16. bundler.py +44 -0
  17. config/__init__.py +0 -0
  18. config/channels.json +11 -0
  19. config/notification.json +17 -0
  20. data_adapters/__init__.py +0 -0
  21. data_adapters/adapter.py +16 -0
  22. data_adapters/base_data_adapter.py +467 -0
  23. data_adapters/file/__init__.py +0 -0
  24. data_adapters/file/adapter.py +2043 -0
  25. data_adapters/file/adapter_helpers.py +1013 -0
  26. data_adapters/file/archive.py +150 -0
  27. data_adapters/file/create_index.py +331 -0
  28. data_adapters/file/create_users_folders.py +52 -0
  29. data_adapters/file/custom_validations.py +68 -0
  30. data_adapters/file/drop_index.py +40 -0
  31. data_adapters/file/health_check.py +560 -0
  32. data_adapters/file/redis_services.py +1110 -0
  33. data_adapters/helpers.py +27 -0
  34. data_adapters/sql/__init__.py +0 -0
  35. data_adapters/sql/adapter.py +3210 -0
  36. data_adapters/sql/adapter_helpers.py +491 -0
  37. data_adapters/sql/create_tables.py +451 -0
  38. data_adapters/sql/create_users_folders.py +53 -0
  39. data_adapters/sql/db_to_json_migration.py +482 -0
  40. data_adapters/sql/health_check_sql.py +232 -0
  41. data_adapters/sql/json_to_db_migration.py +454 -0
  42. data_adapters/sql/update_query_policies.py +101 -0
  43. data_generator.py +81 -0
  44. dmart-0.1.0.dist-info/METADATA +27 -0
  45. dmart-0.1.0.dist-info/RECORD +106 -0
  46. dmart-0.1.0.dist-info/WHEEL +5 -0
  47. dmart-0.1.0.dist-info/entry_points.txt +2 -0
  48. dmart-0.1.0.dist-info/top_level.txt +23 -0
  49. dmart.py +513 -0
  50. get_settings.py +7 -0
  51. languages/__init__.py +0 -0
  52. languages/arabic.json +15 -0
  53. languages/english.json +16 -0
  54. languages/kurdish.json +14 -0
  55. languages/loader.py +13 -0
  56. main.py +506 -0
  57. migrate.py +24 -0
  58. models/__init__.py +0 -0
  59. models/api.py +203 -0
  60. models/core.py +597 -0
  61. models/enums.py +255 -0
  62. password_gen.py +8 -0
  63. plugins/__init__.py +0 -0
  64. pytests/__init__.py +0 -0
  65. pytests/api_user_models_erros_test.py +16 -0
  66. pytests/api_user_models_requests_test.py +98 -0
  67. pytests/archive_test.py +72 -0
  68. pytests/base_test.py +300 -0
  69. pytests/get_settings_test.py +14 -0
  70. pytests/json_to_db_migration_test.py +237 -0
  71. pytests/service_test.py +26 -0
  72. pytests/test_info.py +55 -0
  73. pytests/test_status.py +15 -0
  74. run_notification_campaign.py +98 -0
  75. scheduled_notification_handler.py +121 -0
  76. schema_migration.py +208 -0
  77. schema_modulate.py +192 -0
  78. set_admin_passwd.py +55 -0
  79. sync.py +202 -0
  80. utils/__init__.py +0 -0
  81. utils/access_control.py +306 -0
  82. utils/async_request.py +8 -0
  83. utils/exporter.py +309 -0
  84. utils/firebase_notifier.py +57 -0
  85. utils/generate_email.py +38 -0
  86. utils/helpers.py +352 -0
  87. utils/hypercorn_config.py +12 -0
  88. utils/internal_error_code.py +60 -0
  89. utils/jwt.py +124 -0
  90. utils/logger.py +167 -0
  91. utils/middleware.py +99 -0
  92. utils/notification.py +75 -0
  93. utils/password_hashing.py +16 -0
  94. utils/plugin_manager.py +215 -0
  95. utils/query_policies_helper.py +112 -0
  96. utils/regex.py +44 -0
  97. utils/repository.py +529 -0
  98. utils/router_helper.py +19 -0
  99. utils/settings.py +165 -0
  100. utils/sms_notifier.py +21 -0
  101. utils/social_sso.py +67 -0
  102. utils/templates/activation.html.j2 +26 -0
  103. utils/templates/reminder.html.j2 +17 -0
  104. utils/ticket_sys_utils.py +203 -0
  105. utils/web_notifier.py +29 -0
  106. websocket.py +231 -0
models/enums.py ADDED
@@ -0,0 +1,255 @@
1
+ from enum import StrEnum
2
+ import redis.commands.search.reducers as reducers
3
+
4
+
5
+ class RequestType(StrEnum):
6
+ create = "create"
7
+ update = "update"
8
+ patch = "patch"
9
+ update_acl = "update_acl"
10
+ assign = "assign"
11
+ delete = "delete"
12
+ move = "move"
13
+
14
+
15
+ class Language(StrEnum):
16
+ ar = "arabic"
17
+ en = "english"
18
+ ku = "kurdish"
19
+ fr = "french"
20
+ tr = "turkish"
21
+
22
+ @staticmethod
23
+ def code(lang_str):
24
+ codes = {
25
+ "arabic": "ar",
26
+ "english": "en",
27
+ "kurdish": "ku",
28
+ "french": "fr",
29
+ "turkish": "tr",
30
+ }
31
+ return codes[lang_str]
32
+
33
+
34
+ class PublicSubmitResourceType(StrEnum):
35
+ content = "content"
36
+ ticket = "ticket"
37
+
38
+
39
+ class ResourceType(StrEnum):
40
+ user = "user"
41
+ group = "group"
42
+ folder = "folder"
43
+ schema = "schema"
44
+ content = "content"
45
+ log = "log"
46
+ acl = "acl"
47
+ comment = "comment"
48
+ media = "media"
49
+ data_asset = "data_asset"
50
+ locator = "locator"
51
+ relationship = "relationship"
52
+ alteration = "alteration"
53
+ history = "history"
54
+ space = "space"
55
+ permission = "permission"
56
+ role = "role"
57
+ ticket = "ticket"
58
+ json = "json"
59
+ lock = "lock"
60
+ post = "post"
61
+ reaction = "reaction"
62
+ reply = "reply"
63
+ share = "share"
64
+ plugin_wrapper = "plugin_wrapper"
65
+ notification = "notification"
66
+ csv = "csv"
67
+ jsonl = "jsonl"
68
+ sqlite = "sqlite"
69
+ duckdb = "duckdb"
70
+ parquet = "parquet"
71
+
72
+
73
+ class DataAssetType(StrEnum):
74
+ csv = "csv"
75
+ jsonl = "jsonl"
76
+ sqlite = "sqlite"
77
+ duckdb = "duckdb"
78
+ parquet = "parquet"
79
+
80
+
81
+ class AttachmentType(StrEnum):
82
+ reaction = "reaction"
83
+ share = "share"
84
+ json = "json"
85
+ reply = "reply"
86
+ comment = "comment"
87
+ lock = "lock"
88
+ media = "media"
89
+ data_asset = "data_asset"
90
+ relationship = "relationship"
91
+ alteration = "alteration"
92
+
93
+
94
+ class ContentType(StrEnum):
95
+ text = "text"
96
+ comment = "comment"
97
+ reaction = "reaction"
98
+ markdown = "markdown"
99
+ html = "html"
100
+ json = "json"
101
+ image = "image"
102
+ python = "python"
103
+ pdf = "pdf"
104
+ audio = "audio"
105
+ video = "video"
106
+ csv = "csv"
107
+ parquet = "parquet"
108
+ jsonl = "jsonl"
109
+ apk = "apk"
110
+ duckdb = "duckdb"
111
+ sqlite = "sqlite"
112
+
113
+ @staticmethod
114
+ def inline_types() -> list:
115
+ return [ContentType.text, ContentType.markdown, ContentType.html]
116
+
117
+
118
+ class TaskType(StrEnum):
119
+ query = "query"
120
+
121
+
122
+ class UserType(StrEnum):
123
+ web = "web"
124
+ mobile = "mobile"
125
+ bot = "bot"
126
+
127
+
128
+ class ValidationEnum(StrEnum):
129
+ valid = "valid"
130
+ invalid = "invalid"
131
+
132
+
133
+ class LockAction(StrEnum):
134
+ fetch = "fetch"
135
+ lock = "lock"
136
+ extend = "extend"
137
+ unlock = "unlock"
138
+ cancel = "cancel"
139
+
140
+
141
+ class NotificationType(StrEnum):
142
+ admin = "admin"
143
+ system = "system"
144
+
145
+
146
+ class NotificationPriority(StrEnum):
147
+ high = "high"
148
+ medium = "medium"
149
+ low = "low"
150
+
151
+
152
+ class StrBoolEnum(StrEnum):
153
+ true = "yes"
154
+ false = "no"
155
+
156
+
157
+ class ActionType(StrEnum):
158
+ query = "query"
159
+ view = "view"
160
+ update = "update"
161
+ create = "create"
162
+ delete = "delete"
163
+ attach = "attach"
164
+ assign = "assign"
165
+ move = "move"
166
+ progress_ticket = "progress_ticket"
167
+ lock = "lock"
168
+ unlock = "unlock"
169
+
170
+
171
+ class ConditionType(StrEnum):
172
+ is_active = "is_active"
173
+ own = "own"
174
+
175
+
176
+ class PluginType(StrEnum):
177
+ hook = "hook"
178
+ api = "api"
179
+
180
+
181
+ class EventListenTime(StrEnum):
182
+ before = "before"
183
+ after = "after"
184
+
185
+
186
+ class QueryType(StrEnum):
187
+ search = "search"
188
+ subpath = "subpath"
189
+ events = "events"
190
+ history = "history"
191
+ tags = "tags"
192
+ random = "random"
193
+ spaces = "spaces"
194
+ counters = "counters"
195
+ reports = "reports"
196
+ aggregation = "aggregation"
197
+ attachments = "attachments"
198
+ attachments_aggregation = "attachments_aggregation"
199
+
200
+
201
+ class SortType(StrEnum):
202
+ ascending = "ascending"
203
+ descending = "descending"
204
+
205
+
206
+ class Status(StrEnum):
207
+ success = "success"
208
+ failed = "failed"
209
+
210
+
211
+ class RedisReducerName(StrEnum):
212
+ count_distinct = "count_distinct"
213
+ r_count = "r_count"
214
+ # Same as COUNT_DISTINCT - but provide an approximation instead of an exact count,
215
+ # at the expense of less memory and CPU in big groups
216
+ count_distinctish = "count_distinctish"
217
+ sum = "sum"
218
+ min = "min"
219
+ max = "max"
220
+ avg = "avg"
221
+ # Returns all the matched properties in a list
222
+ tolist = "tolist"
223
+ quantile = "quantile"
224
+ stddev = "stddev"
225
+ # Selects the first value within the group according to sorting parameters
226
+ first_value = "first_value"
227
+ # Returns a random sample of items from the dataset, from the given property
228
+ random_sample = "random_sample"
229
+
230
+ @staticmethod
231
+ def mapper(red: str):
232
+ map = {
233
+ "r_count": reducers.count,
234
+ "count_distinct": reducers.count_distinct,
235
+ "count_distinctish": reducers.count_distinctish,
236
+ "sum": reducers.sum,
237
+ "min": reducers.min,
238
+ "max": reducers.max,
239
+ "avg": reducers.avg,
240
+ "tolist": reducers.tolist,
241
+ "quantile": reducers.quantile,
242
+ "stddev": reducers.stddev,
243
+ "first_value": reducers.first_value,
244
+ "random_sample": reducers.random_sample,
245
+ }
246
+ return map[red]
247
+
248
+
249
+ class ReactionType(StrEnum):
250
+ like = "like"
251
+ dislike = "dislike"
252
+ love = "love"
253
+ care = "care"
254
+ laughing = "laughing"
255
+ sad = "sad"
password_gen.py ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from utils.password_hashing import hash_password
4
+
5
+ print("Enter the password: ", end="")
6
+ password = input()
7
+ print("Generated hash: ", end="")
8
+ print(hash_password(password))
plugins/__init__.py ADDED
File without changes
pytests/__init__.py ADDED
File without changes
@@ -0,0 +1,16 @@
1
+ import pytest
2
+ from models.api import Error
3
+ from utils.internal_error_code import InternalErrorCode
4
+ from api.user.model.errors import INVALID_OTP, EXPIRED_OTP
5
+
6
+ def test_invalid_otp():
7
+ assert isinstance(INVALID_OTP, Error)
8
+ assert INVALID_OTP.type == "OTP"
9
+ assert INVALID_OTP.code == InternalErrorCode.OTP_INVALID
10
+ assert INVALID_OTP.message == "Invalid OTP"
11
+
12
+ def test_expired_otp():
13
+ assert isinstance(EXPIRED_OTP, Error)
14
+ assert EXPIRED_OTP.type == "OTP"
15
+ assert EXPIRED_OTP.code == InternalErrorCode.OTP_EXPIRED
16
+ assert EXPIRED_OTP.message == "Expired OTP"
@@ -0,0 +1,98 @@
1
+ import pytest
2
+ from pydantic import ValidationError
3
+
4
+ from api.user.model.requests import (
5
+ OTPType,
6
+ SendOTPRequest,
7
+ PasswordResetRequest,
8
+ ConfirmOTPRequest,
9
+ UserLoginRequest,
10
+ Exception,
11
+ Error,
12
+ InternalErrorCode
13
+ )
14
+ import utils.regex as rgx
15
+
16
+
17
+ def test_send_otp_request_valid_msisdn():
18
+ request = SendOTPRequest(msisdn="7777778110")
19
+ result = request.check_fields()
20
+ assert result == {"msisdn": "7777778110"}
21
+
22
+ def test_send_otp_request_valid_email():
23
+ request = SendOTPRequest(email="test@example.com")
24
+ result = request.check_fields()
25
+ assert result == {"email": "test@example.com"}
26
+
27
+ def test_send_otp_request_missing_fields():
28
+ # Ensure both fields are explicitly None
29
+ with pytest.raises(Exception) as excinfo:
30
+ SendOTPRequest(msisdn=None, email=None).check_fields()
31
+ assert excinfo.value.status_code == 422
32
+ assert excinfo.value.error.code == InternalErrorCode.EMAIL_OR_MSISDN_REQUIRED
33
+
34
+ def test_send_otp_request_too_many_fields():
35
+ with pytest.raises(Exception) as excinfo:
36
+ SendOTPRequest(msisdn="7777778110", email="test@example.com").check_fields()
37
+ assert excinfo.value.status_code == 422
38
+ assert excinfo.value.error.code == InternalErrorCode.INVALID_STANDALONE_DATA
39
+
40
+ def test_password_reset_request_valid_msisdn():
41
+ request = PasswordResetRequest(msisdn="7777778110")
42
+ result = request.check_fields()
43
+ assert result == {"msisdn": "7777778110"}
44
+
45
+ def test_password_reset_request_valid_email():
46
+ request = PasswordResetRequest(email="test@example.com")
47
+ result = request.check_fields()
48
+ assert result == {"email": "test@example.com"}
49
+
50
+ def test_password_reset_request_missing_fields():
51
+ with pytest.raises(Exception) as excinfo:
52
+ PasswordResetRequest().check_fields()
53
+ assert excinfo.value.status_code == 422
54
+ assert excinfo.value.error.code == InternalErrorCode.EMAIL_OR_MSISDN_REQUIRED
55
+
56
+ def test_password_reset_request_too_many_fields():
57
+ with pytest.raises(Exception) as excinfo:
58
+ PasswordResetRequest(msisdn="7777778110", email="test@example.com").check_fields()
59
+ assert excinfo.value.status_code == 422
60
+ assert excinfo.value.error.code == InternalErrorCode.INVALID_STANDALONE_DATA
61
+
62
+ def test_confirm_otp_request_valid():
63
+ request = ConfirmOTPRequest(msisdn="7777778110", code="123456")
64
+ assert request.msisdn == "7777778110"
65
+ assert request.code == "123456"
66
+
67
+ def test_confirm_otp_request_invalid_code():
68
+ with pytest.raises(ValidationError):
69
+ ConfirmOTPRequest(msisdn="7777778110", code="invalid")
70
+
71
+ def test_user_login_request_valid_shortname():
72
+ request = UserLoginRequest(shortname="john_doo", password="my_secure_password_@_93301")
73
+ result = request.check_fields()
74
+ assert result == {"shortname": "john_doo"}
75
+
76
+ def test_user_login_request_valid_email():
77
+ request = UserLoginRequest(email="test@example.com", password="my_secure_password_@_93301")
78
+ result = request.check_fields()
79
+ assert result == {"email": "test@example.com"}
80
+
81
+ def test_user_login_request_valid_msisdn():
82
+ request = UserLoginRequest(msisdn="7777778110", password="my_secure_password_@_93301")
83
+ result = request.check_fields()
84
+ assert result == {"msisdn": "7777778110"}
85
+
86
+ def test_user_login_request_missing_fields():
87
+ request = UserLoginRequest(password="my_secure_password_@_93301")
88
+ result = request.check_fields()
89
+ assert result == {}
90
+
91
+ def test_user_login_request_too_many_fields():
92
+ with pytest.raises(ValueError, match="Too many input has been passed"):
93
+ UserLoginRequest(shortname="john_doo", email="test@example.com", msisdn="7777778110", password="my_secure_password_@_93301").check_fields()
94
+
95
+ def test_user_login_request_missing_password():
96
+ request = UserLoginRequest(shortname="john_doo")
97
+ result = request.check_fields()
98
+ assert result == {"shortname": "john_doo"}
@@ -0,0 +1,72 @@
1
+ # import pytest
2
+ # from unittest.mock import patch
3
+ # from time import time
4
+ # import argparse
5
+ # import asyncio
6
+ # from archive import redis_doc_to_meta, archive
7
+ #
8
+ #
9
+ # def test_redis_doc_to_meta():
10
+ # mock_record = {
11
+ # "resource_type": "record",
12
+ # "created_at": time(),
13
+ # "updated_at": time(),
14
+ # }
15
+ # expected_keys = ["resource_type", "created_at", "updated_at"]
16
+ # with patch("models.core.Record") as MockRecord:
17
+ # MockRecord.model_fields.keys.return_value = expected_keys
18
+ # MockRecord.model_validate.return_value = mock_record
19
+ # meta = redis_doc_to_meta(mock_record)
20
+ # assert meta == mock_record
21
+ # assert MockRecord.model_fields.keys.call_count == 3
22
+ # MockRecord.model_validate.assert_called_once()
23
+ #
24
+ # def main():
25
+ # parser = argparse.ArgumentParser(
26
+ # description="Script for archiving records from different spaces and subpaths."
27
+ # )
28
+ # parser.add_argument("space", type=str, help="The name of the space")
29
+ # parser.add_argument("subpath", type=str, help="The subpath within the space")
30
+ # parser.add_argument(
31
+ # "schema",
32
+ # type=str,
33
+ # help="The subpath within the space. Optional, if not provided move everything",
34
+ # nargs="?",
35
+ # )
36
+ # parser.add_argument(
37
+ # "olderthan",
38
+ # type=int,
39
+ # help="The number of day, older than which, the entries will be archived (based on updated_at)",
40
+ # )
41
+ #
42
+ # args = parser.parse_args()
43
+ # space = args.space
44
+ # subpath = args.subpath
45
+ # olderthan = args.olderthan
46
+ # schema = args.schema or "meta"
47
+ #
48
+ # asyncio.run(archive(space, subpath, schema, olderthan))
49
+ # print("Done.")
50
+ #
51
+ #
52
+ # @pytest.mark.asyncio
53
+ # @patch("argparse.ArgumentParser.parse_args")
54
+ # @patch("archive.archive")
55
+ # async def test_main(mock_archive, mock_parse_args):
56
+ # mock_args = argparse.Namespace(
57
+ # space="space",
58
+ # subpath="subpath",
59
+ # schema="schema",
60
+ # olderthan=1
61
+ # )
62
+ # mock_parse_args.return_value = mock_args
63
+ #
64
+ # with patch("asyncio.run") as mock_asyncio_run:
65
+ # mock_asyncio_run.side_effect = lambda x: asyncio.ensure_future(x)
66
+ # main()
67
+ #
68
+ # mock_parse_args.assert_called_once()
69
+ # mock_asyncio_run.assert_called_once()
70
+ #
71
+ # if __name__ == "__main__":
72
+ # pytest.main()