android-notify 1.3__py3-none-any.whl → 1.60.6.dev0__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 android-notify might be problematic. Click here for more details.

@@ -1,3 +1,4 @@
1
+ """"For Easier Imports For Public Classes"""
1
2
  from .core import send_notification
2
3
  from .styles import NotificationStyles
3
- from .sword import Notification
4
+ from .sword import Notification,NotificationHandler
@@ -0,0 +1,24 @@
1
+ import argparse
2
+ from .config import __version__
3
+
4
+ def print_version():
5
+ text = f"android_notify: v{__version__}"
6
+ border = '+'+'-'*(len(text) + 2)+'+'
7
+ print(border)
8
+ print(f'| {text} |')
9
+ print(border)
10
+
11
+ def main():
12
+ parser = argparse.ArgumentParser(description="Android Notify CLI")
13
+ parser.add_argument('-v','--version', action='store_true', help="Show the version of android_notify")
14
+ args = parser.parse_args()
15
+
16
+ if args.version:
17
+ print_version()
18
+ # # Placeholder for the main functionality
19
+ # print("Android Notify CLI is running...")
20
+ # DEV: pip install -e ., when edit and test project locally
21
+
22
+
23
+ if __name__ == "__main__":
24
+ main()
@@ -0,0 +1,341 @@
1
+ """For autocomplete Storing Reference to Available Methods"""
2
+ from typing import Literal
3
+
4
+ Importance = Literal['urgent', 'high', 'medium', 'low', 'none']
5
+ """
6
+ :argument urgent - Makes a sound and appears as a heads-up notification.
7
+
8
+ :argument high - Makes a sound.
9
+
10
+ :argument urgent - Makes no sound.
11
+
12
+ :argument urgent - Makes no sound and doesn't appear in the status bar.
13
+
14
+ :argument urgent - Makes no sound and doesn't in the status bar or shade.
15
+ """
16
+
17
+
18
+ # For Dev
19
+ # Idea for typing autocompletion and reference
20
+ class Bundle:
21
+ def putString(self, key, value):
22
+ print(f"[MOCK] Bundle.putString called with key={key}, value={value}")
23
+
24
+ def putInt(self, key, value):
25
+ print(f"[MOCK] Bundle.putInt called with key={key}, value={value}")
26
+
27
+
28
+ class String(str):
29
+ def __new__(cls, value):
30
+ print(f"[MOCK] String created with value={value}")
31
+ return str.__new__(cls, value)
32
+
33
+
34
+ class Intent:
35
+ FLAG_ACTIVITY_NEW_TASK = 'FACADE_FLAG_ACTIVITY_NEW_TASK'
36
+ CATEGORY_DEFAULT = 'FACADE_FLAG_CATEGORY_DEFAULT'
37
+
38
+ def __init__(self, context='', activity=''):
39
+ self.obj = {}
40
+ print(f"[MOCK] Intent initialized with context={context}, activity={activity}")
41
+
42
+ def setAction(self, action):
43
+ print(f"[MOCK] Intent.setAction called with: {action}")
44
+ return self
45
+
46
+ def addFlags(self, *flags):
47
+ print(f"[MOCK] Intent.addFlags called with: {flags}")
48
+ return self
49
+
50
+ def setData(self, uri):
51
+ print(f"[MOCK] Intent.setData called with: {uri}")
52
+ return self
53
+
54
+ def setFlags(self, intent_flag):
55
+ print(f"[MOCK] Intent.setFlags called with: {intent_flag}")
56
+ return self
57
+
58
+ def addCategory(self, intent_category):
59
+ print(f"[MOCK] Intent.addCategory called with: {intent_category}")
60
+ return self
61
+
62
+ def getAction(self):
63
+ print("[MOCK] Intent.getAction called")
64
+ return self
65
+
66
+ def getStringExtra(self, key):
67
+ print(f"[MOCK] Intent.getStringExtra called with key={key}")
68
+ return self
69
+
70
+ def putExtra(self, key, value):
71
+ self.obj[key] = value
72
+ print(f"[MOCK] Intent.putExtra called with key={key}, value={value}")
73
+
74
+ def putExtras(self, bundle: Bundle):
75
+ self.obj['bundle'] = bundle
76
+ print(f"[MOCK] Intent.putExtras called with bundle={bundle}")
77
+
78
+
79
+ class PendingIntent:
80
+ FLAG_IMMUTABLE = ''
81
+ FLAG_UPDATE_CURRENT = ''
82
+
83
+ def getActivity(self, context, value, action_intent, pending_intent_type):
84
+ print(
85
+ f"[MOCK] PendingIntent.getActivity called with context={context}, value={value}, action_intent={action_intent}, type={pending_intent_type}")
86
+
87
+
88
+ class BitmapFactory:
89
+ def decodeStream(self, stream):
90
+ print(f"[MOCK] BitmapFactory.decodeStream called with stream={stream}")
91
+
92
+
93
+ class BuildVersion:
94
+ SDK_INT = 0
95
+
96
+ class Manifest:
97
+ POST_NOTIFICATIONS = 'FACADE_IMPORT'
98
+
99
+ class Settings:
100
+ ACTION_APP_NOTIFICATION_SETTINGS = 'FACADE_IMPORT_ACTION_APP_NOTIFICATION_SETTINGS'
101
+ EXTRA_APP_PACKAGE = 'FACADE_IMPORT_EXTRA_APP_PACKAGE'
102
+ ACTION_APPLICATION_DETAILS_SETTINGS = 'FACADE_IMPORT_ACTION_APPLICATION_DETAILS_SETTINGS'
103
+
104
+ class Uri:
105
+ def __init__(self,package_name):
106
+ print("FACADE_URI")
107
+
108
+ class NotificationManager:
109
+ pass
110
+
111
+ class NotificationManagerClass:
112
+ pass
113
+
114
+
115
+ class NotificationChannel:
116
+ def __init__(self, channel_id, channel_name, importance):
117
+ self.description = None
118
+ self.channel_id = channel_id
119
+ self.channel = None
120
+ print(
121
+ f"[MOCK] NotificationChannel initialized with id={channel_id}, name={channel_name}, importance={importance}")
122
+
123
+ def createNotificationChannel(self, channel):
124
+ self.channel = channel
125
+ print(f"[MOCK] NotificationChannel.createNotificationChannel called with channel={channel}")
126
+
127
+ def getNotificationChannel(self, channel_id):
128
+ self.channel_id = channel_id
129
+ print(f"[MOCK] NotificationChannel.getNotificationChannel called with id={channel_id}")
130
+
131
+ def setDescription(self, description):
132
+ self.description = description
133
+ print(f"[MOCK] NotificationChannel.setDescription called with description={description}")
134
+
135
+ def getId(self):
136
+ print(f"[MOCK] NotificationChannel.getId called, returning {self.channel_id}")
137
+ return self.channel_id
138
+
139
+
140
+ class IconCompat:
141
+ def createWithBitmap(self, bitmap):
142
+ print(f"[MOCK] IconCompat.createWithBitmap called with bitmap={bitmap}")
143
+
144
+
145
+ class Color:
146
+ def __init__(self):
147
+ print("[MOCK] Color initialized")
148
+
149
+ def parseColor(self, color: str):
150
+ print(f"[MOCK] Color.parseColor called with color={color}")
151
+ return self
152
+
153
+
154
+ class RemoteViews:
155
+ def __init__(self, package_name, small_layout_id):
156
+ print(f"[MOCK] RemoteViews initialized with package_name={package_name}, layout_id={small_layout_id}")
157
+
158
+ def createWithBitmap(self, bitmap):
159
+ print(f"[MOCK] RemoteViews.createWithBitmap called with bitmap={bitmap}")
160
+
161
+ def setTextViewText(self, id, text):
162
+ print(f"[MOCK] RemoteViews.setTextViewText called with id={id}, text={text}")
163
+
164
+ def setTextColor(self, id, color: Color):
165
+ print(f"[MOCK] RemoteViews.setTextColor called with id={id}, color={color}")
166
+
167
+
168
+ class NotificationManagerCompat:
169
+ IMPORTANCE_HIGH = 4
170
+ IMPORTANCE_DEFAULT = 3
171
+ IMPORTANCE_LOW = ''
172
+ IMPORTANCE_MIN = ''
173
+ IMPORTANCE_NONE = ''
174
+
175
+ class AndroidNotification:
176
+ DEFAULT_ALL = 3
177
+ PRIORITY_HIGH = 4
178
+ PRIORITY_DEFAULT = ''
179
+ PRIORITY_LOW = ''
180
+ PRIORITY_MIN = ''
181
+
182
+ class NotificationCompat:
183
+ DEFAULT_ALL = 3
184
+ PRIORITY_HIGH = 4
185
+ PRIORITY_DEFAULT = ''
186
+ PRIORITY_LOW = ''
187
+ PRIORITY_MIN = ''
188
+
189
+
190
+ class MActions:
191
+ def clear(self):
192
+ """This Removes all buttons"""
193
+ print('[MOCK] MActions.clear called')
194
+
195
+
196
+ class NotificationCompatBuilder:
197
+ def __init__(self, context, channel_id):
198
+ self.mActions = MActions()
199
+ print(f"[MOCK] NotificationCompatBuilder initialized with context={context}, channel_id={channel_id}")
200
+
201
+ def setProgress(self, max_value, current_value, endless):
202
+ print(f"[MOCK] setProgress called with max={max_value}, current={current_value}, endless={endless}")
203
+
204
+ def setStyle(self, style):
205
+ print(f"[MOCK] setStyle called with style={style}")
206
+
207
+ def setContentTitle(self, title):
208
+ print(f"[MOCK] setContentTitle called with title={title}")
209
+
210
+ def setContentText(self, text):
211
+ print(f"[MOCK] setContentText called with text={text}")
212
+
213
+ def setSmallIcon(self, icon):
214
+ print(f"[MOCK] setSmallIcon called with icon={icon}")
215
+
216
+ def setLargeIcon(self, icon):
217
+ print(f"[MOCK] setLargeIcon called with icon={icon}")
218
+
219
+ def setAutoCancel(self, auto_cancel: bool):
220
+ print(f"[MOCK] setAutoCancel called with auto_cancel={auto_cancel}")
221
+
222
+ def setPriority(self, priority: Importance):
223
+ print(f"[MOCK] setPriority called with priority={priority}")
224
+
225
+ def setDefaults(self, defaults):
226
+ print(f"[MOCK] setDefaults called with defaults={defaults}")
227
+
228
+ def setOngoing(self, persistent: bool):
229
+ print(f"[MOCK] setOngoing called with persistent={persistent}")
230
+
231
+ def setOnlyAlertOnce(self, state):
232
+ print(f"[MOCK] setOnlyAlertOnce called with state={state}")
233
+
234
+ def build(self):
235
+ print("[MOCK] build called")
236
+
237
+ def setContentIntent(self, pending_action_intent: PendingIntent):
238
+ print(f"[MOCK] setContentIntent called with {pending_action_intent}")
239
+
240
+ def addAction(self, icon_int, action_text, pending_action_intent):
241
+ print(f"[MOCK] addAction called with icon={icon_int}, text={action_text}, intent={pending_action_intent}")
242
+
243
+ def setShowWhen(self, state):
244
+ print(f"[MOCK] setShowWhen called with state={state}")
245
+
246
+ def setWhen(self, time_ms):
247
+ print(f"[MOCK] setWhen called with time_ms={time_ms}")
248
+
249
+ def setCustomContentView(self, layout):
250
+ print(f"[MOCK] setCustomContentView called with layout={layout}")
251
+
252
+ def setCustomBigContentView(self, layout):
253
+ print(f"[MOCK] setCustomBigContentView called with layout={layout}")
254
+
255
+ def setSubText(self, text):
256
+ print(f"[MOCK] setSubText called with text={text}")
257
+
258
+ def setColor(self, color: Color) -> None:
259
+ print(f"[MOCK] setColor called with color={color}")
260
+
261
+
262
+ class NotificationCompatBigTextStyle:
263
+ def bigText(self, body):
264
+ print(f"[MOCK] NotificationCompatBigTextStyle.bigText called with body={body}")
265
+ return self
266
+
267
+
268
+ class NotificationCompatBigPictureStyle:
269
+ def bigPicture(self, bitmap):
270
+ print(f"[MOCK] NotificationCompatBigPictureStyle.bigPicture called with bitmap={bitmap}")
271
+ return self
272
+
273
+
274
+ class NotificationCompatInboxStyle:
275
+ def addLine(self, line):
276
+ print(f"[MOCK] NotificationCompatInboxStyle.addLine called with line={line}")
277
+ return self
278
+
279
+
280
+ class NotificationCompatDecoratedCustomViewStyle:
281
+ def __init__(self):
282
+ print("[MOCK] NotificationCompatDecoratedCustomViewStyle initialized")
283
+
284
+
285
+ class Permission:
286
+ POST_NOTIFICATIONS = ''
287
+
288
+
289
+ def check_permission(permission: Permission.POST_NOTIFICATIONS):
290
+ print(f"[MOCK] check_permission called with {permission}")
291
+ print(permission)
292
+
293
+
294
+ def request_permissions(_list: [], _callback):
295
+ print(f"[MOCK] request_permissions called with {_list}")
296
+ _callback()
297
+
298
+
299
+ class AndroidActivity:
300
+ def bind(self, on_new_intent):
301
+ print(f"[MOCK] AndroidActivity.bind called with {on_new_intent}")
302
+
303
+ def unbind(self, on_new_intent):
304
+ print(f"[MOCK] AndroidActivity.unbind called with {on_new_intent}")
305
+
306
+
307
+ class PythonActivity:
308
+ mActivity = "[MOCK] mActivity used"
309
+ def __init__(self):
310
+ print("[MOCK] PythonActivity initialized")
311
+
312
+
313
+ class DummyIcon:
314
+ icon = 101
315
+
316
+ def __init__(self):
317
+ print("[MOCK] DummyIcon initialized")
318
+
319
+
320
+ class Context:
321
+ def __init__(self):
322
+ print("[MOCK] Context initialized")
323
+ pass
324
+
325
+ @staticmethod
326
+ def getApplicationInfo():
327
+ print("[MOCK] Context.getApplicationInfo called")
328
+ return DummyIcon
329
+
330
+ @staticmethod
331
+ def getResources():
332
+ print("[MOCK] Context.getResources called")
333
+ return None
334
+
335
+ @staticmethod
336
+ def getPackageName():
337
+ print("[MOCK] Context.getPackageName called")
338
+ return None # TODO get package name from buildozer.spec file
339
+
340
+ # Now writing Knowledge from errors
341
+ # notify.(int, Builder.build()) # must be int
@@ -0,0 +1,209 @@
1
+ """Collection of useful functions"""
2
+
3
+ import inspect, os, re, traceback
4
+ from .config import autoclass
5
+ from .an_types import Importance
6
+ from .config import (
7
+ get_python_activity_context, app_storage_path, ON_ANDROID,
8
+ BitmapFactory, BuildVersion, Bundle,
9
+ NotificationManagerClass, AndroidNotification, Intent, Settings, Uri, String, Manifest
10
+
11
+ )
12
+
13
+ if ON_ANDROID:
14
+ Color = autoclass('android.graphics.Color')
15
+ else:
16
+ from .an_types import Color
17
+
18
+
19
+ def can_accept_arguments(func, *args, **kwargs):
20
+ try:
21
+ sig = inspect.signature(func)
22
+ sig.bind(*args, **kwargs)
23
+ return True
24
+ except TypeError:
25
+ return False
26
+
27
+
28
+ if ON_ANDROID:
29
+ context = get_python_activity_context()
30
+ else:
31
+ context = None
32
+
33
+
34
+ def get_android_importance(importance: Importance):
35
+ """
36
+ Returns Android Importance Values
37
+ :param importance: ['urgent','high','medium','low','none']
38
+ :return: Android equivalent int or empty str
39
+ """
40
+ if not ON_ANDROID:
41
+ return None
42
+ value = ''
43
+ if importance == 'urgent':
44
+ value = AndroidNotification.PRIORITY_HIGH if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_HIGH
45
+ elif importance == 'high':
46
+ value = AndroidNotification.PRIORITY_DEFAULT if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_DEFAULT
47
+ elif importance == 'medium':
48
+ value = AndroidNotification.PRIORITY_LOW if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_LOW
49
+ elif importance == 'low':
50
+ value = AndroidNotification.PRIORITY_MIN if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_MIN
51
+ elif importance == 'none':
52
+ value = '' if BuildVersion.SDK_INT <= 25 else NotificationManagerClass.IMPORTANCE_NONE
53
+
54
+ return value
55
+ # side-note 'medium' = NotificationCompat.PRIORITY_LOW and 'low' = NotificationCompat.PRIORITY_MIN # weird but from docs
56
+
57
+
58
+ def generate_channel_id(channel_name: str) -> str:
59
+ """
60
+ Generate a readable and consistent channel ID from a channel name.
61
+
62
+ Args:
63
+ channel_name (str): The name of the notification channel.
64
+
65
+ Returns:
66
+ str: A sanitized channel ID.
67
+ """
68
+ # Normalize the channel name
69
+ channel_id = channel_name.strip().lower()
70
+ # Replace spaces and special characters with underscores
71
+ channel_id = re.sub(r'[^a-z0-9]+', '_', channel_id)
72
+ # Remove leading/trailing underscores
73
+ channel_id = channel_id.strip('_')
74
+ return channel_id[:50]
75
+
76
+
77
+ def get_img_from_path(relative_path):
78
+ app_folder = os.path.join(app_storage_path(), 'app')
79
+ img_full_path = os.path.join(app_folder, relative_path)
80
+ if not os.path.exists(img_full_path):
81
+ print(f'\nImage: "{img_full_path}" Not Found, (Local images gotten from App Path)')
82
+ try:
83
+ print("- These are the existing files in your app Folder:")
84
+ print('[' + ', '.join(os.listdir(app_folder)) + ']\n')
85
+ except Exception as could_not_get_files_in_path_error:
86
+ print('Exception: ', could_not_get_files_in_path_error)
87
+ print("Couldn't get Files in App Folder")
88
+ return None
89
+ return get_bitmap_from_path(img_full_path)
90
+ # TODO test with a badly written Image and catch error
91
+
92
+
93
+ def setLayoutText(layout, id, text, color):
94
+ # checked if self.title_color available before entering method
95
+ if id and text:
96
+ layout.setTextViewText(id, text)
97
+ if color:
98
+ layout.setTextColor(id, Color.parseColor(color))
99
+
100
+
101
+ def get_bitmap_from_url(url, callback, logs):
102
+ """Gets Bitmap from url
103
+
104
+ Args:
105
+ :param url: img url
106
+ :param callback: function to be called after thread done, callback receives bitmap data as argument
107
+ :param logs:
108
+ """
109
+ if logs:
110
+ print("getting Bitmap from URL---")
111
+ try:
112
+ URL = autoclass('java.net.URL')
113
+ url = URL(url)
114
+ connection = url.openConnection()
115
+ connection.connect()
116
+ input_stream = connection.getInputStream()
117
+ bitmap = BitmapFactory.decodeStream(input_stream)
118
+ input_stream.close()
119
+ if bitmap:
120
+ callback(bitmap)
121
+ else:
122
+ print('Error No Bitmap for small icon ------------')
123
+ except Exception as extracting_bitmap_frm_URL_error:
124
+ callback(None)
125
+ # TODO get all types of JAVA Error that can fail here
126
+ print('Error Type ', extracting_bitmap_frm_URL_error)
127
+ print('Failed to get Bitmap from URL ', traceback.format_exc())
128
+
129
+
130
+ def add_data_to_intent(intent, title):
131
+ """Persist Some data to notification object for later use"""
132
+ bundle = Bundle()
133
+ bundle.putString("title", title or 'Title Placeholder')
134
+ # bundle.putInt("notify_id", self.__id)
135
+ bundle.putInt("notify_id", 101)
136
+ intent.putExtras(bundle)
137
+
138
+
139
+ def get_sound_uri(res_sound_name):
140
+ if not res_sound_name: # Incase it's None
141
+ return None
142
+
143
+ package_name = context.getPackageName()
144
+ return Uri.parse(f"android.resource://{package_name}/raw/{res_sound_name}")
145
+
146
+
147
+ def get_package_path():
148
+ """
149
+ Returns the directory path of this Python package.
150
+ Works on Android, Windows, Linux, macOS.
151
+ """
152
+ return os.path.dirname(os.path.abspath(__file__))
153
+
154
+
155
+ def get_bitmap_from_path(img_full_path):
156
+ uri = Uri.parse(f"file://{img_full_path}")
157
+ return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri))
158
+
159
+
160
+ def icon_finder(icon_name):
161
+ """Get the full path to an icon file."""
162
+ try:
163
+ import pkg_resources
164
+ return pkg_resources.resource_filename(__name__, f"fallback-icons/{icon_name}")
165
+ except Exception:
166
+ # Fallback if pkg_resources not available
167
+ package_dir = get_package_path()
168
+ return os.path.join(package_dir, "fallback-icons", icon_name)
169
+
170
+
171
+ def can_show_permission_request_popup():
172
+ """
173
+ Check if we can show permission request popup for POST_NOTIFICATIONS
174
+ :return: bool
175
+ """
176
+ if not ON_ANDROID:
177
+ return False
178
+
179
+ if BuildVersion.SDK_INT < 33:
180
+ return False
181
+
182
+ return context.shouldShowRequestPermissionRationale(Manifest.POST_NOTIFICATIONS)
183
+
184
+
185
+ def open_settings_screen():
186
+ if not context:
187
+ print("android_notify - Can't open settings screen, No context [not On Android]")
188
+ return None
189
+ intent = Intent()
190
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
191
+ package_name = String(context.getPackageName()) # String() is very important else fails silently with a toast
192
+ # saying "The app wasn't found in the list of installed apps" - Xiaomi or "unable to find application to perform this action" - Samsung and Techno
193
+
194
+ if BuildVersion.SDK_INT >= 26: # Android 8.0 - android.os.Build.VERSION_CODES.O
195
+ intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
196
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, package_name)
197
+ elif BuildVersion.SDK_INT >= 22: # Android 5.0 - Build.VERSION_CODES.LOLLIPOP
198
+ intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS")
199
+ intent.putExtra("app_package", package_name)
200
+ intent.putExtra("app_uid", context.getApplicationInfo().uid)
201
+ else: # Last Retort is to open App Settings Screen
202
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
203
+ intent.addCategory(Intent.CATEGORY_DEFAULT)
204
+ intent.setData(Uri.parse("package:" + package_name))
205
+
206
+ context.startActivity(intent)
207
+ return None
208
+
209
+ # https://stackoverflow.com/a/45192258/19961621
android_notify/base.py ADDED
@@ -0,0 +1,97 @@
1
+ """Assists Notification Class with Args keeps subclass cleaner"""
2
+ from dataclasses import dataclass, fields
3
+ import difflib
4
+ from .styles import NotificationStyles
5
+ # For Dev when creating new attr use have to set type for validate_args to work
6
+
7
+ @dataclass
8
+ class BaseNotification:
9
+ """Encapsulation"""
10
+
11
+ # Basic options
12
+ title: str = ''
13
+ message: str = ''
14
+ style: str = 'simple'
15
+
16
+ # Style-specific attributes
17
+ big_picture_path: str = ''
18
+ large_icon_path: str = ''
19
+ progress_max_value: int = 0
20
+ progress_current_value: float = 0.0 # Also Takes in Ints
21
+ body: str = ''
22
+ lines_txt: str = ''
23
+
24
+ # Notification Functions
25
+ name: str = ''
26
+ callback: object = None
27
+
28
+ # Advanced Options
29
+ id: int = 0
30
+ app_icon: str = 'Defaults to package app icon'
31
+ sub_text: str=''
32
+
33
+ # Channel related
34
+ channel_name: str = 'Default Channel'
35
+ """User visible channel name"""
36
+ channel_id: str = 'default_channel'
37
+ """Used to reference notification channel"""
38
+
39
+ silent: bool = False
40
+ logs: bool = False
41
+
42
+ # Custom Notification Attrs
43
+ title_color: str = ''
44
+ message_color: str = ''
45
+
46
+ def __init__(self, **kwargs):
47
+ """Custom init to handle validation before dataclass assigns values"""
48
+
49
+ # Validate provided arguments
50
+ self.validate_args(kwargs)
51
+
52
+ # Assign validated values using the normal dataclass behavior
53
+ for field_ in fields(self):
54
+ field_name = field_.name
55
+ setattr(self, field_name, kwargs.get(field_name, getattr(self, field_name)))
56
+
57
+ def validate_args(self, inputted_kwargs):
58
+ """Check for unexpected arguments and suggest corrections before Python validation"""
59
+ default_fields = {field.name : field.type for field in fields(self)} #{'title': <class 'str'>, 'message': <class 'str'>,...
60
+ allowed_fields_keys = set(default_fields.keys())
61
+
62
+ # Identify invalid arguments
63
+ invalid_args = set(inputted_kwargs) - allowed_fields_keys
64
+ if invalid_args:
65
+ suggestions = []
66
+ for arg in invalid_args:
67
+ closest_match = difflib.get_close_matches(arg, allowed_fields_keys, n=1, cutoff=0.6)
68
+ if closest_match:
69
+ suggestions.append(f"* '{arg}' is invalid -> Did you mean '{closest_match[0]}'?")
70
+ else:
71
+ suggestions.append(f"* '{arg}' is not a valid argument.")
72
+
73
+ suggestion_text = '\n'.join(suggestions)
74
+ raise ValueError(f"Invalid arguments provided:\n{suggestion_text}")
75
+
76
+ # Validating types
77
+ for each_arg in inputted_kwargs.keys():
78
+ expected_type = default_fields[each_arg]
79
+ actual_value = inputted_kwargs[each_arg]
80
+
81
+ # Allow both int and float for progress_current_value
82
+ if each_arg == "progress_current_value":
83
+ if not isinstance(actual_value, (int, float)):
84
+ raise TypeError(f"Expected '{each_arg}' to be int or float, got {type(actual_value)} instead.")
85
+ else:
86
+ if not isinstance(actual_value, expected_type):
87
+ raise TypeError(f"Expected '{each_arg}' to be {expected_type}, got {type(actual_value)} instead.")
88
+
89
+ # Validate `style` values
90
+ style_values = [value for key, value in vars(NotificationStyles).items() if not key.startswith("__")]
91
+ if 'style' in inputted_kwargs and inputted_kwargs['style'] not in ['',*style_values]:
92
+ inputted_style=inputted_kwargs['style']
93
+ allowed_styles=', '.join(style_values)
94
+ raise ValueError(
95
+ f"Invalid style '{inputted_style}'. Allowed styles: {allowed_styles}"
96
+ )
97
+