rgwfuncs 0.0.103__tar.gz → 0.0.107__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rgwfuncs
3
- Version: 0.0.103
3
+ Version: 0.0.107
4
4
  Summary: A functional programming paradigm for mathematical modelling and data science
5
5
  Home-page: https://github.com/ryangerardwilson/rgwfuncs
6
6
  Author: Ryan Gerard Wilson
@@ -24,6 +24,7 @@ Requires-Dist: requests
24
24
  Requires-Dist: slack-sdk
25
25
  Requires-Dist: google-api-python-client
26
26
  Requires-Dist: boto3
27
+ Requires-Dist: pyfiglet
27
28
  Dynamic: license-file
28
29
 
29
30
  # RGWFUNCS
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rgwfuncs"
7
- version = "0.0.103"
7
+ version = "0.0.107"
8
8
  authors = [
9
9
  { name = "Ryan Gerard Wilson", email = "ryangerardwilson@gmail.com" },
10
10
  ]
@@ -27,7 +27,8 @@ dependencies = [
27
27
  "requests",
28
28
  "slack-sdk",
29
29
  "google-api-python-client",
30
- "boto3"
30
+ "boto3",
31
+ "pyfiglet"
31
32
  ]
32
33
 
33
34
  dynamic = ["scripts"]
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = rgwfuncs
3
- version = 0.0.103
3
+ version = 0.0.107
4
4
  author = Ryan Gerard Wilson
5
5
  author_email = ryangerardwilson@gmail.com
6
6
  description = A functional programming paradigm for mathematical modelling and data science
@@ -29,6 +29,7 @@ install_requires =
29
29
  slack-sdk
30
30
  google-api-python-client
31
31
  boto3
32
+ pyfiglet
32
33
 
33
34
  [options.packages.find]
34
35
  where = src
@@ -4,4 +4,4 @@
4
4
  from .df_lib import append_columns, append_percentile_classification_column, append_ranged_classification_column, append_ranged_date_classification_column, append_rows, append_xgb_labels, append_xgb_logistic_regression_predictions, append_xgb_regression_predictions, bag_union_join, bottom_n_unique_values, cascade_sort, delete_rows, drop_duplicates, drop_duplicates_retain_first, drop_duplicates_retain_last, filter_dataframe, filter_indian_mobiles, first_n_rows, from_raw_data, insert_dataframe_in_sqlite_database, last_n_rows, left_join, limit_dataframe, load_data_from_path, load_data_from_query, load_data_from_sqlite_path, load_fresh_data_or_pull_from_cache, mask_against_dataframe, mask_against_dataframe_converse, numeric_clean, order_columns, print_correlation, print_dataframe, print_memory_usage, print_n_frequency_cascading, print_n_frequency_linear, rename_columns, retain_columns, right_join, send_data_to_email, send_data_to_slack, send_dataframe_via_telegram, sync_dataframe_to_sqlite_database, top_n_unique_values, union_join, update_rows
5
5
  from .interactive_shell_lib import interactive_shell
6
6
  from .docs_lib import docs
7
- from .str_lib import send_telegram_message
7
+ from .str_lib import heading, send_telegram_message, sub_heading, title
@@ -0,0 +1,276 @@
1
+ import sys
2
+ import time
3
+ import os
4
+ import json
5
+ import requests
6
+ from typing import Tuple, Optional, Union, Dict
7
+ from collections import defaultdict
8
+ import warnings
9
+ from pyfiglet import Figlet
10
+ from datetime import datetime
11
+
12
+ # Suppress all FutureWarnings
13
+ warnings.filterwarnings("ignore", category=FutureWarning)
14
+
15
+ # Module-level variables
16
+ _PRINT_HEADING_CURRENT_CALL = 0
17
+ _PRINT_SUBHEADING_COUNTS = defaultdict(int) # Tracks sub-headings per heading
18
+ _CURRENT_HEADING_NUMBER = 0
19
+
20
+ def send_telegram_message(preset_name: str, message: str, config: Optional[Union[str, dict]] = None) -> None:
21
+ """
22
+ Send a Telegram message using the specified preset.
23
+
24
+ Args:
25
+ preset_name (str): The name of the preset to use for sending the message.
26
+ message (str): The message to send.
27
+ config (Optional[Union[str, dict]], optional): Configuration source. Can be:
28
+ - None: Searches for '.rgwfuncsrc' in current directory and upwards
29
+ - str: Path to a JSON configuration file
30
+ - dict: Direct configuration dictionary
31
+
32
+ Raises:
33
+ FileNotFoundError: If no '.rgwfuncsrc' file is found in current or parent directories.
34
+ ValueError: If the config parameter is neither a path string nor a dictionary, or if the config file is empty/invalid.
35
+ RuntimeError: If the preset is not found or necessary details are missing.
36
+ """
37
+ def get_config(config: Optional[Union[str, dict]] = None) -> dict:
38
+ """Get configuration either from a path, direct dictionary, or by searching upwards."""
39
+ def get_config_from_file(config_path: str) -> dict:
40
+ """Load configuration from a JSON file."""
41
+ # print(f"Reading config from: {config_path}") # Debug line
42
+ with open(config_path, 'r', encoding='utf-8') as file:
43
+ content = file.read()
44
+ # print(f"Config content (first 100 chars): {content[:100]}...") # Debug line
45
+ if not content.strip():
46
+ raise ValueError(f"Config file {config_path} is empty")
47
+ try:
48
+ return json.loads(content)
49
+ except json.JSONDecodeError as e:
50
+ raise ValueError(f"Invalid JSON in config file {config_path}: {e}")
51
+
52
+ def find_config_file() -> str:
53
+ """Search for '.rgwfuncsrc' in current directory and upwards."""
54
+ current_dir = os.getcwd()
55
+ # print(f"Starting config search from: {current_dir}") # Debug line
56
+ while True:
57
+ config_path = os.path.join(current_dir, '.rgwfuncsrc')
58
+ # print(f"Checking for config at: {config_path}") # Debug line
59
+ if os.path.isfile(config_path):
60
+ # print(f"Found config at: {config_path}") # Debug line
61
+ return config_path
62
+ parent_dir = os.path.dirname(current_dir)
63
+ if parent_dir == current_dir: # Reached root directory
64
+ raise FileNotFoundError(f"No '.rgwfuncsrc' file found in {os.getcwd()} or parent directories")
65
+ current_dir = parent_dir
66
+
67
+ # Determine the config to use
68
+ if config is None:
69
+ # Search for .rgwfuncsrc upwards from current directory
70
+ config_path = find_config_file()
71
+ return get_config_from_file(config_path)
72
+ elif isinstance(config, str):
73
+ # If config is a string, treat it as a path and load it
74
+ return get_config_from_file(config)
75
+ elif isinstance(config, dict):
76
+ # If config is already a dict, use it directly
77
+ return config
78
+ else:
79
+ raise ValueError("Config must be either a path string or a dictionary")
80
+
81
+ def get_telegram_preset(config: dict, preset_name: str) -> dict:
82
+ """Get the Telegram preset configuration."""
83
+ presets = config.get("telegram_bot_presets", [])
84
+ for preset in presets:
85
+ if preset.get("name") == preset_name:
86
+ return preset
87
+ return None
88
+
89
+ def get_telegram_bot_details(config: dict, preset_name: str) -> Tuple[str, str]:
90
+ """Retrieve the Telegram bot token and chat ID from the preset."""
91
+ preset = get_telegram_preset(config, preset_name)
92
+ if not preset:
93
+ raise RuntimeError(
94
+ f"Telegram bot preset '{preset_name}' not found in the configuration file")
95
+
96
+ bot_token = preset.get("bot_token")
97
+ chat_id = preset.get("chat_id")
98
+
99
+ if not bot_token or not chat_id:
100
+ raise RuntimeError(
101
+ f"Telegram bot token or chat ID for '{preset_name}' not found in the configuration file")
102
+
103
+ return bot_token, chat_id
104
+
105
+ config = get_config(config)
106
+ # Get bot details from the configuration
107
+ bot_token, chat_id = get_telegram_bot_details(config, preset_name)
108
+
109
+ # Prepare the request
110
+ url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
111
+ payload = {"chat_id": chat_id, "text": message}
112
+
113
+ # Send the message
114
+ response = requests.post(url, json=payload)
115
+ response.raise_for_status()
116
+
117
+ def title(text: str, font: str = "slant", typing_speed: float = 0.005) -> None:
118
+ """
119
+ Print text as ASCII art with a typewriter effect using the specified font (default: slant),
120
+ indented by 4 spaces. All output, including errors and separators, is printed with the
121
+ typewriter effect within this function.
122
+
123
+ Args:
124
+ text (str): The text to convert to ASCII art.
125
+ font (str, optional): The pyfiglet font to use. Defaults to "slant".
126
+ typing_speed (float, optional): Delay between printing each character in seconds.
127
+ Defaults to 0.005.
128
+
129
+ Raises:
130
+ ValueError: If the specified font is invalid or unavailable.
131
+ RuntimeError: If there is an error generating the ASCII art.
132
+ """
133
+ # ANSI color codes
134
+ heading_color = '\033[92m' # Bright green for headings
135
+ reset_color = '\033[0m' # Reset to default
136
+
137
+ try:
138
+ # Initialize Figlet with the specified font
139
+ figlet = Figlet(font=font)
140
+ # Generate ASCII art
141
+ ascii_art = figlet.renderText(text)
142
+ # Indent each line by 4 spaces
143
+ indented_ascii_art = '\n'.join(' ' + line for line in ascii_art.splitlines())
144
+
145
+ # Print ASCII art with typewriter effect
146
+ print(heading_color, end='')
147
+ for char in indented_ascii_art + '\n':
148
+ print(char, end='', flush=True)
149
+ if char != '\n': # Don't delay on newlines
150
+ time.sleep(typing_speed)
151
+ print(reset_color, end='')
152
+
153
+ # Print separator line with typewriter effect
154
+ print(heading_color, end='')
155
+ for char in '=' * 79 + '\n':
156
+ print(char, end='', flush=True)
157
+ if char != '\n':
158
+ time.sleep(typing_speed)
159
+ print(reset_color, end='')
160
+
161
+ except Exception as e:
162
+ error_msg = ''
163
+ if "font" in str(e).lower():
164
+ error_msg = f"Invalid or unavailable font: {font}. Ensure the font is supported by pyfiglet.\n"
165
+ print(reset_color, end='')
166
+ for char in error_msg:
167
+ print(char, end='', flush=True)
168
+ if char != '\n':
169
+ time.sleep(typing_speed)
170
+ raise ValueError(error_msg)
171
+ error_msg = f"Error generating ASCII art for \"{text}\" with font {font}: {e}\n"
172
+ print(reset_color, end='')
173
+ for char in error_msg:
174
+ print(char, end='', flush=True)
175
+ if char != '\n':
176
+ time.sleep(typing_speed)
177
+ raise RuntimeError(error_msg)
178
+
179
+ def heading(text: str, typing_speed: float = 0.002) -> None:
180
+ """
181
+ Print a heading with the specified text in uppercase,
182
+ formatted as '[current_call] TEXT' with a timestamp and typewriter effect.
183
+ Ensures the formatted heading is <= 50 characters and total line is 79 characters.
184
+ Adds empty lines before and after the heading.
185
+
186
+ Args:
187
+ text (str): The heading text to print.
188
+ typing_speed (float, optional): Delay between printing each character in seconds.
189
+ Defaults to 0.005.
190
+ """
191
+ global _PRINT_HEADING_CURRENT_CALL, _CURRENT_HEADING_NUMBER, _PRINT_SUBHEADING_COUNTS
192
+
193
+ # Increment heading call
194
+ _PRINT_HEADING_CURRENT_CALL += 1
195
+ _CURRENT_HEADING_NUMBER = _PRINT_HEADING_CURRENT_CALL
196
+ _PRINT_SUBHEADING_COUNTS[_CURRENT_HEADING_NUMBER] = 0 # Reset sub-heading count
197
+
198
+ # ANSI color codes
199
+ color = '\033[92m' # Bright green for headings
200
+ reset_color = '\033[0m'
201
+
202
+ # Format heading
203
+ prefix = f"[{_PRINT_HEADING_CURRENT_CALL}] "
204
+ max_text_length = 50 - len(prefix)
205
+ formatted_text = text.upper()[:max_text_length]
206
+ heading = f"{prefix}{formatted_text}"
207
+
208
+ # Get timestamp
209
+ timestamp = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
210
+
211
+ # Calculate padding
212
+ padding_length = 79 - len(heading) - 1 - len(timestamp) - 1 # Spaces before padding and timestamp
213
+ padding = '=' * padding_length if padding_length > 0 else ''
214
+ full_line = f"{heading} {padding} {timestamp}"
215
+
216
+ # Print with line breaks and typewriter effect
217
+ print() # Empty line before
218
+ print(color, end='')
219
+ for char in full_line + '\n':
220
+ print(char, end='', flush=True)
221
+ if char != '\n':
222
+ time.sleep(typing_speed)
223
+ print(reset_color, end='')
224
+ print() # Empty line after
225
+
226
+ def sub_heading(text: str, typing_speed: float = 0.002) -> None:
227
+ """
228
+ Print a sub-heading under the most recent heading, formatted as
229
+ '[heading_num.sub_heading_num] TEXT' with a timestamp and typewriter effect.
230
+ Ensures the formatted sub-heading is <= 50 characters and total line is 79 characters.
231
+ Adds empty lines before and after the sub-heading.
232
+
233
+ Args:
234
+ text (str): The sub-heading text to print.
235
+ typing_speed (float, optional): Delay between printing each character in seconds.
236
+ Defaults to 0.005.
237
+
238
+ Raises:
239
+ ValueError: If no heading has been called.
240
+ """
241
+ global _PRINT_SUBHEADING_COUNTS, _CURRENT_HEADING_NUMBER
242
+
243
+ if _CURRENT_HEADING_NUMBER == 0:
244
+ raise ValueError("No heading called before sub_heading.")
245
+
246
+ # Increment sub-heading count
247
+ _PRINT_SUBHEADING_COUNTS[_CURRENT_HEADING_NUMBER] += 1
248
+ current_sub = _PRINT_SUBHEADING_COUNTS[_CURRENT_HEADING_NUMBER]
249
+
250
+ # ANSI color codes
251
+ color = '\033[92m' # Bright green for sub-headings
252
+ reset_color = '\033[0m'
253
+
254
+ # Format sub-heading
255
+ prefix = f"[{_CURRENT_HEADING_NUMBER}.{current_sub}] "
256
+ max_text_length = 50 - len(prefix)
257
+ formatted_text = text.lower()[:max_text_length]
258
+ sub_heading = f"{prefix}{formatted_text}"
259
+
260
+ # Get timestamp
261
+ timestamp = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]"
262
+
263
+ # Calculate padding
264
+ padding_length = 79 - len(sub_heading) - 1 - len(timestamp) - 1 # Spaces before padding and timestamp
265
+ padding = '-' * padding_length if padding_length > 0 else ''
266
+ full_line = f"{sub_heading} {padding} {timestamp}"
267
+
268
+ # Print with line breaks and typewriter effect
269
+ print() # Empty line before
270
+ print(color, end='')
271
+ for char in full_line + '\n':
272
+ print(char, end='', flush=True)
273
+ if char != '\n':
274
+ time.sleep(typing_speed)
275
+ print(reset_color, end='')
276
+ print() # Empty line after
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rgwfuncs
3
- Version: 0.0.103
3
+ Version: 0.0.107
4
4
  Summary: A functional programming paradigm for mathematical modelling and data science
5
5
  Home-page: https://github.com/ryangerardwilson/rgwfuncs
6
6
  Author: Ryan Gerard Wilson
@@ -24,6 +24,7 @@ Requires-Dist: requests
24
24
  Requires-Dist: slack-sdk
25
25
  Requires-Dist: google-api-python-client
26
26
  Requires-Dist: boto3
27
+ Requires-Dist: pyfiglet
27
28
  Dynamic: license-file
28
29
 
29
30
  # RGWFUNCS
@@ -9,3 +9,4 @@ requests
9
9
  slack-sdk
10
10
  google-api-python-client
11
11
  boto3
12
+ pyfiglet
@@ -1,106 +0,0 @@
1
- import os
2
- import json
3
- import requests
4
- from typing import Tuple, Optional, Union, Dict
5
- import warnings
6
-
7
- # Suppress all FutureWarnings
8
- warnings.filterwarnings("ignore", category=FutureWarning)
9
-
10
-
11
- def send_telegram_message(preset_name: str, message: str, config: Optional[Union[str, dict]] = None) -> None:
12
- """
13
- Send a Telegram message using the specified preset.
14
-
15
- Args:
16
- preset_name (str): The name of the preset to use for sending the message.
17
- message (str): The message to send.
18
- config (Optional[Union[str, dict]], optional): Configuration source. Can be:
19
- - None: Searches for '.rgwfuncsrc' in current directory and upwards
20
- - str: Path to a JSON configuration file
21
- - dict: Direct configuration dictionary
22
-
23
- Raises:
24
- FileNotFoundError: If no '.rgwfuncsrc' file is found in current or parent directories.
25
- ValueError: If the config parameter is neither a path string nor a dictionary, or if the config file is empty/invalid.
26
- RuntimeError: If the preset is not found or necessary details are missing.
27
- """
28
- def get_config(config: Optional[Union[str, dict]] = None) -> dict:
29
- """Get configuration either from a path, direct dictionary, or by searching upwards."""
30
- def get_config_from_file(config_path: str) -> dict:
31
- """Load configuration from a JSON file."""
32
- # print(f"Reading config from: {config_path}") # Debug line
33
- with open(config_path, 'r', encoding='utf-8') as file:
34
- content = file.read()
35
- # print(f"Config content (first 100 chars): {content[:100]}...") # Debug line
36
- if not content.strip():
37
- raise ValueError(f"Config file {config_path} is empty")
38
- try:
39
- return json.loads(content)
40
- except json.JSONDecodeError as e:
41
- raise ValueError(f"Invalid JSON in config file {config_path}: {e}")
42
-
43
- def find_config_file() -> str:
44
- """Search for '.rgwfuncsrc' in current directory and upwards."""
45
- current_dir = os.getcwd()
46
- # print(f"Starting config search from: {current_dir}") # Debug line
47
- while True:
48
- config_path = os.path.join(current_dir, '.rgwfuncsrc')
49
- # print(f"Checking for config at: {config_path}") # Debug line
50
- if os.path.isfile(config_path):
51
- # print(f"Found config at: {config_path}") # Debug line
52
- return config_path
53
- parent_dir = os.path.dirname(current_dir)
54
- if parent_dir == current_dir: # Reached root directory
55
- raise FileNotFoundError(f"No '.rgwfuncsrc' file found in {os.getcwd()} or parent directories")
56
- current_dir = parent_dir
57
-
58
- # Determine the config to use
59
- if config is None:
60
- # Search for .rgwfuncsrc upwards from current directory
61
- config_path = find_config_file()
62
- return get_config_from_file(config_path)
63
- elif isinstance(config, str):
64
- # If config is a string, treat it as a path and load it
65
- return get_config_from_file(config)
66
- elif isinstance(config, dict):
67
- # If config is already a dict, use it directly
68
- return config
69
- else:
70
- raise ValueError("Config must be either a path string or a dictionary")
71
-
72
- def get_telegram_preset(config: dict, preset_name: str) -> dict:
73
- """Get the Telegram preset configuration."""
74
- presets = config.get("telegram_bot_presets", [])
75
- for preset in presets:
76
- if preset.get("name") == preset_name:
77
- return preset
78
- return None
79
-
80
- def get_telegram_bot_details(config: dict, preset_name: str) -> Tuple[str, str]:
81
- """Retrieve the Telegram bot token and chat ID from the preset."""
82
- preset = get_telegram_preset(config, preset_name)
83
- if not preset:
84
- raise RuntimeError(
85
- f"Telegram bot preset '{preset_name}' not found in the configuration file")
86
-
87
- bot_token = preset.get("bot_token")
88
- chat_id = preset.get("chat_id")
89
-
90
- if not bot_token or not chat_id:
91
- raise RuntimeError(
92
- f"Telegram bot token or chat ID for '{preset_name}' not found in the configuration file")
93
-
94
- return bot_token, chat_id
95
-
96
- config = get_config(config)
97
- # Get bot details from the configuration
98
- bot_token, chat_id = get_telegram_bot_details(config, preset_name)
99
-
100
- # Prepare the request
101
- url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
102
- payload = {"chat_id": chat_id, "text": message}
103
-
104
- # Send the message
105
- response = requests.post(url, json=payload)
106
- response.raise_for_status()
File without changes
File without changes