django-nativemojo 0.1.10__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 (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. testit/runner.py +262 -0
testit/helpers.py ADDED
@@ -0,0 +1,198 @@
1
+ import sys
2
+ from objict import objict
3
+ from mojo.helpers import logit
4
+ import functools
5
+ import traceback
6
+
7
+
8
+ TEST_RUN = objict(
9
+ total=0, passed=0, failed=0,
10
+ tests=objict(active_test=None),
11
+ results=objict())
12
+ STOP_ON_FAIL = True
13
+ VERBOSE = False
14
+ INDENT = " "
15
+
16
+
17
+ class TestitAbort(Exception):
18
+ pass
19
+
20
+
21
+ def _run_setup(func, *args, **kwargs):
22
+ name = kwargs.get("name", func.__name__)
23
+ logit.color_print(f"{INDENT}{name.ljust(60, '.')}", logit.ConsoleLogger.PINK, end="")
24
+ res = func(*args, **kwargs)
25
+ logit.color_print("DONE", logit.ConsoleLogger.PINK, end="\n")
26
+ return res
27
+
28
+
29
+ def unit_setup():
30
+ """
31
+ Decorator to mark a function as a test setup function.
32
+ Will be run before each test in the test class.
33
+
34
+ Usage:
35
+ @unit_setup()
36
+ def setup():
37
+ # Setup code here
38
+ pass
39
+ """
40
+ def decorator(func):
41
+ @functools.wraps(func)
42
+ def wrapper(*args, **kwargs):
43
+ return _run_setup(func, *args, **kwargs)
44
+ wrapper._is_setup = True
45
+ return wrapper
46
+ return decorator
47
+
48
+
49
+ def django_unit_setup():
50
+ """
51
+ Decorator to mark a function as a test setup function.
52
+ Will be run before each test in the test class.
53
+
54
+ Usage:
55
+ @django_setup()
56
+ def setup():
57
+ # Setup code here
58
+ pass
59
+ """
60
+ def decorator(func):
61
+ @functools.wraps(func)
62
+ def wrapper(*args, **kwargs):
63
+ import os
64
+ import django
65
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings')
66
+ django.setup()
67
+ return _run_setup(func, *args, **kwargs)
68
+ wrapper._is_setup = True
69
+ return wrapper
70
+ return decorator
71
+
72
+
73
+ def _run_unit(func, name, *args, **kwargs):
74
+ TEST_RUN.total += 1
75
+ if name:
76
+ test_name = name
77
+ else:
78
+ test_name = kwargs.get("test_name", func.__name__)
79
+ if test_name.startswith("test_"):
80
+ test_name = test_name[5:]
81
+
82
+ # Print test start message
83
+ logit.color_print(f"{INDENT}{test_name.ljust(60, '.')}", logit.ConsoleLogger.YELLOW, end="")
84
+
85
+ try:
86
+ result = func(*args, **kwargs)
87
+ TEST_RUN.results[f"{TEST_RUN.active_test}:{test_name}"] = True
88
+ TEST_RUN.passed += 1
89
+
90
+ logit.color_print("PASSED", logit.ConsoleLogger.GREEN, end="\n")
91
+ return result
92
+
93
+ except AssertionError as error:
94
+ TEST_RUN.failed += 1
95
+ TEST_RUN.results[f"{TEST_RUN.active_test}:{test_name}"] = False
96
+
97
+ # Print failure message
98
+ logit.color_print("FAILED", logit.ConsoleLogger.RED, end="\n")
99
+ logit.color_print(f"{INDENT}{INDENT}{error}", logit.ConsoleLogger.PINK)
100
+
101
+ if STOP_ON_FAIL:
102
+ raise TestitAbort()
103
+
104
+ except Exception as error:
105
+ TEST_RUN.failed += 1
106
+ TEST_RUN.results[f"{TEST_RUN.active_test}:{test_name}"] = False
107
+
108
+ # Print error message
109
+ logit.color_print("FAILED", logit.ConsoleLogger.RED, end="\n")
110
+ if VERBOSE:
111
+ logit.color_print(traceback.format_exc(), logit.ConsoleLogger.PINK)
112
+ if STOP_ON_FAIL:
113
+ raise TestitAbort()
114
+ return False
115
+
116
+ # Test Decorator
117
+ def unit_test(name=None):
118
+ """
119
+ Decorator to track unit test execution.
120
+
121
+ Usage:
122
+ @unit_test("Custom Test Name")
123
+ def my_test():
124
+ assert 1 == 1
125
+ """
126
+ def decorator(func):
127
+ @functools.wraps(func)
128
+ def wrapper(*args, **kwargs):
129
+ _run_unit(func, name, *args, **kwargs)
130
+ return wrapper
131
+ return decorator
132
+
133
+
134
+ # Test Decorator
135
+ def django_unit_test(name=None):
136
+ """
137
+ Decorator to track unit test execution.
138
+
139
+ Usage:
140
+ @unit_test("Custom Test Name")
141
+ def my_test():
142
+ assert 1 == 1
143
+ """
144
+ def decorator(func):
145
+ @functools.wraps(func)
146
+ def wrapper(*args, **kwargs):
147
+ import os
148
+ import django
149
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings')
150
+ django.setup()
151
+ _run_unit(func, name, *args, **kwargs)
152
+ return wrapper
153
+ return decorator
154
+
155
+
156
+ def get_mock_request(user=None, ip="127.0.0.1", path='/', method='GET', META=None):
157
+ """
158
+ Creates a mock Django request object with a user and request.ip information.
159
+
160
+ Args:
161
+ user (User, optional): A mock user object. Defaults to None.
162
+ ip (str, optional): The IP address for the request. Defaults to "127.0.0.1".
163
+ path (str, optional): The path for the request. Defaults to '/'.
164
+ method (str, optional): The HTTP method for the request. Defaults to 'GET'.
165
+ META (dict, optional): Additional metadata for the request.
166
+ Merges with default if provided. Defaults to None.
167
+
168
+ Returns:
169
+ objict: A mock request object with request.ip, request.user, and additional attributes.
170
+ """
171
+ request = objict()
172
+ request.ip = ip
173
+ request.user = user if user else get_mock_user()
174
+ default_META = {
175
+ 'SERVER_PROTOCOL': 'HTTP/1.1',
176
+ 'QUERY_STRING': '',
177
+ 'HTTP_USER_AGENT': 'Mozilla/5.0',
178
+ 'HTTP_HOST': 'localhost',
179
+ }
180
+ request.META = {**default_META, **(META or {})}
181
+ request.method = method
182
+ request.path = path
183
+ return request
184
+
185
+ def get_mock_user():
186
+ """
187
+ Creates a mock user object.
188
+
189
+ Returns:
190
+ objict: A mock user object with basic attributes.
191
+ """
192
+ user = objict()
193
+ user.id = 1
194
+ user.username = "mockuser"
195
+ user.email = "mockuser@example.com"
196
+ user.is_authenticated = True
197
+ user.has_permission = lambda perm: True
198
+ return user
testit/runner.py ADDED
@@ -0,0 +1,262 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ import traceback
5
+ import inspect
6
+ import argparse
7
+ from importlib import import_module
8
+
9
+ from mojo.helpers import logit
10
+ from testit import helpers
11
+ import testit.client
12
+
13
+ from mojo.helpers import paths
14
+ TEST_ROOT = paths.APPS_ROOT / "tests"
15
+
16
+ def get_host():
17
+ """Extract host and port from dev_server.conf."""
18
+ host = "127.0.0.1"
19
+ port = 8001
20
+ try:
21
+ config_path = paths.CONFIG_ROOT / "dev_server.conf"
22
+ with open(config_path, 'r') as file:
23
+ for line in file:
24
+ if line.startswith("host"):
25
+ host = line.split('=')[1].strip()
26
+ elif line.startswith("port"):
27
+ port = line.split('=')[1].strip()
28
+ except FileNotFoundError:
29
+ print("Configuration file not found.")
30
+ except Exception as e:
31
+ print(f"Error reading configuration: {e}")
32
+ return f"http://{host}:{port}"
33
+
34
+ def setup_parser():
35
+ """Setup command-line arguments for the test runner."""
36
+ parser = argparse.ArgumentParser(description="Django Test Runner")
37
+
38
+ parser.add_argument("-v", "--verbose", action="store_true",
39
+ help="Enable verbose logging")
40
+ parser.add_argument("-f", "--force", action="store_true",
41
+ help="Force the test to run now")
42
+ parser.add_argument("-u", "--user", type=str, default="nobody",
43
+ help="Specify the user the test should run as")
44
+ parser.add_argument("-m", "--module", type=str, default=None,
45
+ help="Run only this app/module")
46
+ parser.add_argument("--method", type=str, default=None,
47
+ help="Run only a specific test method")
48
+ parser.add_argument("-t", "--test", type=str, default=None,
49
+ help="Specify a specific test method to run")
50
+ parser.add_argument("-q", "--quick", action="store_true",
51
+ help="Run only tests flagged as critical/quick")
52
+ parser.add_argument("-x", "--extra", type=str, default=None,
53
+ help="Specify extra data to pass to test")
54
+ parser.add_argument("-l", "--list", action="store_true",
55
+ help="List available tests instead of running them")
56
+ parser.add_argument("-s", "--stop", action="store_true",
57
+ help="Stop on errors")
58
+ parser.add_argument("-e", "--errors", action="store_true",
59
+ help="Show errors")
60
+ parser.add_argument("--host", type=str, default=get_host(),
61
+ help="Specify host for API tests")
62
+ parser.add_argument("--setup", action="store_true",
63
+ help="Run setup before executing tests")
64
+ parser.add_argument("--nomojo", action="store_true",
65
+ help="Do not run Mojo app tests")
66
+ parser.add_argument("--onlymojo", action="store_true",
67
+ help="Only run Mojo app tests")
68
+ return parser.parse_args()
69
+
70
+
71
+ def run_test(opts, module, func_name, module_name, test_name):
72
+ """Run a specific test function inside a module."""
73
+ test_key = f"{module_name}.{test_name}.{func_name}"
74
+ helpers.VERBOSE = opts.verbose
75
+ helpers.TEST_RUN.tests.active_test = test_key.replace(".", ":")
76
+ try:
77
+ getattr(module, func_name)(opts)
78
+ except Exception as err:
79
+ if opts.verbose:
80
+ print(f"⚠️ Test Error: {err}")
81
+ if opts.stop:
82
+ sys.exit(1)
83
+
84
+
85
+ def run_setup(opts, module, func_name, module_name, test_name):
86
+ """Run a specific test function inside a module."""
87
+ test_key = f"{module_name}.{test_name}.{func_name}"
88
+ helpers.VERBOSE = opts.verbose
89
+ try:
90
+ getattr(module, func_name)(opts)
91
+ except Exception as err:
92
+ if opts.verbose:
93
+ print(f"⚠️ Setup Error: {err}")
94
+ if opts.stop:
95
+ sys.exit(1)
96
+
97
+
98
+ def import_module_for_testing(module_name, test_name):
99
+ """Dynamically import a test module."""
100
+ try:
101
+ name = f"{module_name}.{test_name}"
102
+ module = import_module(name)
103
+ return module
104
+ except ImportError:
105
+ print(f"⚠️ Failed to import test module: {name}")
106
+ traceback.print_exc()
107
+ return None
108
+
109
+
110
+ def run_module_tests_by_name(opts, module_name, test_name):
111
+ """Run all test functions in a specific test module in the order they appear."""
112
+ module = import_module_for_testing(module_name, test_name)
113
+ if not module:
114
+ return
115
+ run_module_setup(opts, module, test_name, module_name)
116
+ run_module_tests(opts, module, test_name, module_name)
117
+
118
+
119
+ def run_module_setup(opts, module, test_name, module_name):
120
+ opts.client = testit.client.RestClient(opts.host, logger=opts.logger)
121
+ test_key = f"{module_name}.{test_name}"
122
+ started = time.time()
123
+ prefix = "setup_"
124
+
125
+ # Get all functions in the module
126
+ functions = inspect.getmembers(module, inspect.isfunction)
127
+
128
+ # Preserve definition order by using inspect.getsourcelines()
129
+ functions = sorted(
130
+ functions,
131
+ key=lambda func: inspect.getsourcelines(func[1])[1] # Sort by line number
132
+ )
133
+ setup_funcs = []
134
+ for func_name, func in functions:
135
+ if func_name.startswith(prefix):
136
+ setup_funcs.append((module, func_name))
137
+
138
+ if len(setup_funcs):
139
+ logit.color_print(f"\nRUNNING SETUP: {test_key}", logit.ConsoleLogger.BLUE)
140
+ for module, func_name in setup_funcs:
141
+ run_setup(opts, module, func_name, module_name, test_name)
142
+ duration = time.time() - started
143
+ print(f"{helpers.INDENT}---------\n{helpers.INDENT}run time: {duration:.2f}s")
144
+
145
+
146
+ def run_module_tests(opts, module, test_name, module_name):
147
+ opts.client = testit.client.RestClient(opts.host, logger=opts.logger)
148
+ test_key = f"{module_name}.{test_name}"
149
+ logit.color_print(f"\nRUNNING TEST: {test_key}", logit.ConsoleLogger.BLUE)
150
+ started = time.time()
151
+ prefix = "test_" if not opts.quick else "quick_"
152
+
153
+ # Get all functions in the module
154
+ functions = inspect.getmembers(module, inspect.isfunction)
155
+
156
+ # Preserve definition order by using inspect.getsourcelines()
157
+ functions = sorted(
158
+ functions,
159
+ key=lambda func: inspect.getsourcelines(func[1])[1] # Sort by line number
160
+ )
161
+
162
+ for func_name, func in functions:
163
+ if func_name.startswith(prefix):
164
+ run_test(opts, module, func_name, module_name, test_name)
165
+
166
+ duration = time.time() - started
167
+ print(f"{helpers.INDENT}---------\n{helpers.INDENT}run time: {duration:.2f}s")
168
+
169
+
170
+ def run_tests_for_module(opts, module_name, test_root, parent_test_root=None):
171
+ """Discover and run tests for a given module."""
172
+ module_path = os.path.join(test_root, module_name)
173
+ if not os.path.exists(module_path):
174
+ if parent_test_root is None:
175
+ raise FileNotFoundError(f"Module '{module_name}' not found")
176
+ module_path = os.path.join(parent_test_root, module_name)
177
+ if not os.path.exists(module_path):
178
+ raise FileNotFoundError(f"Module '{module_name}' not found")
179
+ test_files = [f for f in os.listdir(module_path)
180
+ if f.endswith(".py") and f not in ["__init__.py", "setup.py"]]
181
+
182
+ for test_file in sorted(test_files):
183
+ test_name = test_file.rsplit('.', 1)[0] # Remove .py extension
184
+ run_module_tests_by_name(opts, module_name, test_name)
185
+
186
+
187
+ def setup_modules():
188
+ """Run setup scripts for all test modules."""
189
+ logit.color_print("\n[TEST PREFLIGHT SETUP]\n", logit.ConsoleLogger.BLUE)
190
+
191
+ test_modules = [d for d in os.listdir(TEST_ROOT) if os.path.isdir(os.path.join(TEST_ROOT, d))]
192
+
193
+ for module_name in sorted(test_modules):
194
+ setup_file = os.path.join(TEST_ROOT, module_name, "setup.py")
195
+ if os.path.isfile(setup_file):
196
+ module = import_module_for_testing(module_name, "setup")
197
+ if module and hasattr(module, "run_setup"):
198
+ logit.color_print(f"Setting up {module_name}...", logit.ConsoleLogger.YELLOW)
199
+ module.run_setup(opts)
200
+ logit.color_print("✔ DONE\n", logit.ConsoleLogger.GREEN)
201
+
202
+
203
+ def main(opts):
204
+ """Main function to run tests."""
205
+ if opts.setup:
206
+ setup_modules()
207
+
208
+ if opts.list:
209
+ print("\n------------------------")
210
+ print("Listing available test modules & tests")
211
+ print("[module]")
212
+ print(" [test1]")
213
+ print(" [test2]")
214
+ print("------------------------")
215
+ return
216
+
217
+ opts.logger = logit.get_logger("testit", "testit.log")
218
+
219
+ if opts.module and '.' in opts.module:
220
+ opts.module, opts.test = opts.module.split('.', 1)
221
+
222
+ parent_test_root = os.path.join(os.path.dirname(os.path.dirname(__file__)), "tests")
223
+ parent_test_modules = None
224
+ if os.path.exists(parent_test_root):
225
+ sys.path.insert(0, parent_test_root)
226
+ parent_test_modules = sorted([d for d in os.listdir(parent_test_root) if os.path.isdir(os.path.join(parent_test_root, d))])
227
+
228
+ if opts.module and opts.test:
229
+ run_module_tests_by_name(opts, opts.module, opts.test)
230
+ elif opts.module:
231
+ run_tests_for_module(opts, opts.module, TEST_ROOT, parent_test_root)
232
+ else:
233
+ test_root = os.path.join(paths.APPS_ROOT, "tests")
234
+ test_modules = sorted([d for d in os.listdir(test_root) if os.path.isdir(os.path.join(test_root, d))])
235
+ if parent_test_modules and not opts.nomojo:
236
+ for module_name in parent_test_modules:
237
+ run_tests_for_module(opts, module_name, parent_test_root)
238
+ if not opts.onlymojo:
239
+ for module_name in test_modules:
240
+ run_tests_for_module(opts, module_name, test_root)
241
+
242
+ # Summary Output
243
+ print("\n" + "=" * 80)
244
+
245
+ logit.color_print(f"TOTAL RUN: {helpers.TEST_RUN.total}\t", logit.ConsoleLogger.YELLOW)
246
+ logit.color_print(f"TOTAL PASSED: {helpers.TEST_RUN.passed}", logit.ConsoleLogger.GREEN)
247
+ if helpers.TEST_RUN.failed > 0:
248
+ logit.color_print(f"TOTAL FAILED: {helpers.TEST_RUN.failed}", logit.ConsoleLogger.RED)
249
+
250
+ print("=" * 80)
251
+
252
+ # Save Test Results
253
+ helpers.TEST_RUN.save(os.path.join(paths.VAR_ROOT, "test_results.json"))
254
+
255
+ # Exit with failure status if any test failed
256
+ if helpers.TEST_RUN.failed > 0:
257
+ sys.exit("❌ Tests failed!")
258
+
259
+
260
+ if __name__ == "__main__":
261
+ opts = setup_parser()
262
+ main(opts)