velocity-python 0.0.31__tar.gz → 0.0.32__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.
Files changed (62) hide show
  1. {velocity_python-0.0.31 → velocity_python-0.0.32}/PKG-INFO +1 -1
  2. {velocity_python-0.0.31 → velocity_python-0.0.32}/pyproject.toml +1 -1
  3. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/__init__.py +1 -1
  4. velocity_python-0.0.32/src/velocity/aws/handlers/response.py +290 -0
  5. velocity_python-0.0.32/src/velocity/misc/conv/iconv.py +185 -0
  6. velocity_python-0.0.32/src/velocity/misc/conv/oconv.py +170 -0
  7. velocity_python-0.0.32/src/velocity/misc/export.py +157 -0
  8. velocity_python-0.0.32/src/velocity/misc/format.py +79 -0
  9. velocity_python-0.0.32/src/velocity/misc/mail.py +78 -0
  10. velocity_python-0.0.32/src/velocity/misc/merge.py +52 -0
  11. velocity_python-0.0.32/src/velocity/misc/timer.py +50 -0
  12. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity_python.egg-info/PKG-INFO +1 -1
  13. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity_python.egg-info/SOURCES.txt +11 -1
  14. velocity_python-0.0.32/tests/test_db.py +90 -0
  15. velocity_python-0.0.32/tests/test_email_processing.py +112 -0
  16. velocity_python-0.0.32/tests/test_format.py +64 -0
  17. velocity_python-0.0.32/tests/test_iconv.py +141 -0
  18. velocity_python-0.0.32/tests/test_merge.py +82 -0
  19. velocity_python-0.0.32/tests/test_oconv.py +140 -0
  20. velocity_python-0.0.32/tests/test_postgres.py +205 -0
  21. velocity_python-0.0.32/tests/test_response.py +156 -0
  22. velocity_python-0.0.32/tests/test_spreadsheet_functions.py +124 -0
  23. velocity_python-0.0.32/tests/test_timer.py +74 -0
  24. velocity_python-0.0.31/src/velocity/aws/handlers/response.py +0 -139
  25. velocity_python-0.0.31/src/velocity/misc/conv/iconv.py +0 -222
  26. velocity_python-0.0.31/src/velocity/misc/conv/oconv.py +0 -208
  27. velocity_python-0.0.31/src/velocity/misc/export.py +0 -154
  28. velocity_python-0.0.31/src/velocity/misc/format.py +0 -82
  29. velocity_python-0.0.31/src/velocity/misc/mail.py +0 -74
  30. velocity_python-0.0.31/src/velocity/misc/merge.py +0 -36
  31. velocity_python-0.0.31/src/velocity/misc/timer.py +0 -27
  32. {velocity_python-0.0.31 → velocity_python-0.0.32}/LICENSE +0 -0
  33. {velocity_python-0.0.31 → velocity_python-0.0.32}/README.md +0 -0
  34. {velocity_python-0.0.31 → velocity_python-0.0.32}/setup.cfg +0 -0
  35. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/aws/__init__.py +0 -0
  36. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/aws/handlers/__init__.py +0 -0
  37. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/aws/handlers/context.py +0 -0
  38. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/aws/handlers/lambda_handler.py +0 -0
  39. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/aws/handlers/sqs_handler.py +0 -0
  40. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/__init__.py +0 -0
  41. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/__init__.py +0 -0
  42. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/column.py +0 -0
  43. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/database.py +0 -0
  44. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/decorators.py +0 -0
  45. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/engine.py +0 -0
  46. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/exceptions.py +0 -0
  47. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/result.py +0 -0
  48. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/row.py +0 -0
  49. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/sequence.py +0 -0
  50. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/table.py +0 -0
  51. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/core/transaction.py +0 -0
  52. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/servers/__init__.py +0 -0
  53. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/servers/mysql.py +0 -0
  54. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/servers/postgres.py +0 -0
  55. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/servers/sqlite.py +0 -0
  56. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/db/servers/sqlserver.py +0 -0
  57. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/misc/__init__.py +0 -0
  58. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/misc/conv/__init__.py +0 -0
  59. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity/misc/db.py +0 -0
  60. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity_python.egg-info/dependency_links.txt +0 -0
  61. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity_python.egg-info/requires.txt +0 -0
  62. {velocity_python-0.0.31 → velocity_python-0.0.32}/src/velocity_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: velocity-python
3
- Version: 0.0.31
3
+ Version: 0.0.32
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Paul Perez <pperez@codeclubs.org>
6
6
  Project-URL: Homepage, https://codeclubs.org/projects/velocity
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "velocity-python"
3
- version = "0.0.31"
3
+ version = "0.0.32"
4
4
  authors = [
5
5
  { name="Paul Perez", email="pperez@codeclubs.org" },
6
6
  ]
@@ -1,4 +1,4 @@
1
- __version__ = version = "0.0.31"
1
+ __version__ = version = "0.0.32"
2
2
 
3
3
  from . import aws
4
4
  from . import db
@@ -0,0 +1,290 @@
1
+ import sys
2
+ import traceback
3
+ from typing import Any, Dict, List, Optional, Union
4
+ from velocity.misc.format import to_json
5
+ from support.app import DEBUG
6
+
7
+
8
+ class Response:
9
+ """Class to manage and structure HTTP responses with various actions and custom headers."""
10
+
11
+ VALID_VARIANTS = {"success", "error", "warning", "info"}
12
+
13
+ def __init__(self):
14
+ """Initialize the Response object with default status, headers, and an empty actions list."""
15
+ self.actions: List[Dict[str, Any]] = []
16
+ self.body: Dict[str, Any] = {"actions": self.actions}
17
+ self.raw: Dict[str, Any] = {
18
+ "statusCode": 200,
19
+ "body": "{}",
20
+ "headers": {
21
+ "Content-Type": "application/json",
22
+ "Access-Control-Allow-Headers": "*",
23
+ "Access-Control-Allow-Origin": "*",
24
+ "Access-Control-Allow-Methods": "OPTIONS,POST,GET",
25
+ },
26
+ }
27
+
28
+ def render(self) -> Dict[str, Any]:
29
+ """
30
+ Finalize the response body as JSON and return the complete response dictionary.
31
+
32
+ Returns:
33
+ Dict[str, Any]: The complete HTTP response with headers, status code, and JSON body.
34
+ """
35
+ self.raw["body"] = to_json(self.body)
36
+ return self.raw
37
+
38
+ def alert(self, message: str, title: str = "Notification") -> "Response":
39
+ """
40
+ Add an alert action to the response.
41
+
42
+ Args:
43
+ message (str): The alert message.
44
+ title (str): Title for the alert. Defaults to "Notification".
45
+
46
+ Returns:
47
+ Response: The current Response object, allowing method chaining.
48
+ """
49
+ self.actions.append(
50
+ {
51
+ "action": "alert",
52
+ "payload": {"title": title, "message": message},
53
+ }
54
+ )
55
+ return self
56
+
57
+ def toast(self, message: str, variant: str = "success") -> "Response":
58
+ """
59
+ Add a toast notification action to the response with a specified variant.
60
+
61
+ Args:
62
+ message (str): The message to display in the toast.
63
+ variant (str): The style variant of the toast (e.g., "success", "error"). Must be one of VALID_VARIANTS.
64
+
65
+ Raises:
66
+ ValueError: If the variant is not one of VALID_VARIANTS.
67
+
68
+ Returns:
69
+ Response: The current Response object, allowing method chaining.
70
+ """
71
+ variant = variant.lower()
72
+ if variant not in self.VALID_VARIANTS:
73
+ raise ValueError(
74
+ f"Notistack variant '{variant}' not in {self.VALID_VARIANTS}"
75
+ )
76
+ self.actions.append(
77
+ {
78
+ "action": "toast",
79
+ "payload": {"options": {"variant": variant}, "message": message},
80
+ }
81
+ )
82
+ return self
83
+
84
+ def load_object(self, payload: Dict[str, Any]) -> "Response":
85
+ """
86
+ Add a load-object action to the response with a specified payload.
87
+
88
+ Args:
89
+ payload (Dict[str, Any]): The data to load into the response.
90
+
91
+ Returns:
92
+ Response: The current Response object, allowing method chaining.
93
+ """
94
+ self.actions.append({"action": "load-object", "payload": payload})
95
+ return self
96
+
97
+ def update_store(self, payload: Dict[str, Any]) -> "Response":
98
+ """
99
+ Add an update-store action to the response with a specified payload.
100
+
101
+ Args:
102
+ payload (Dict[str, Any]): The data to update the store with.
103
+
104
+ Returns:
105
+ Response: The current Response object, allowing method chaining.
106
+ """
107
+ self.actions.append({"action": "update-store", "payload": payload})
108
+ return self
109
+
110
+ def file_download(self, payload: Dict[str, Any]) -> "Response":
111
+ """
112
+ Add a file-download action to the response with a specified payload.
113
+
114
+ Args:
115
+ payload (Dict[str, Any]): The data for file download details.
116
+
117
+ Returns:
118
+ Response: The current Response object, allowing method chaining.
119
+ """
120
+ self.actions.append({"action": "file-download", "payload": payload})
121
+ return self
122
+
123
+ def status(self, code: Optional[int] = None) -> int:
124
+ """
125
+ Get or set the status code of the response.
126
+
127
+ Args:
128
+ code (Optional[int]): The HTTP status code to set. If None, returns the current status code.
129
+
130
+ Returns:
131
+ int: The current status code.
132
+ """
133
+ if code is not None:
134
+ self.raw["statusCode"] = int(code)
135
+ return self.raw["statusCode"]
136
+
137
+ def headers(self, headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
138
+ """
139
+ Get or update the headers of the response.
140
+
141
+ Args:
142
+ headers (Optional[Dict[str, str]]): A dictionary of headers to add or update.
143
+
144
+ Returns:
145
+ Dict[str, str]: The current headers after updates.
146
+ """
147
+ if headers:
148
+ formatted_headers = {
149
+ self._format_header_key(k): v for k, v in headers.items()
150
+ }
151
+ self.raw["headers"].update(formatted_headers)
152
+ return self.raw["headers"]
153
+
154
+ def set_status(self, code: int) -> "Response":
155
+ """
156
+ Set the HTTP status code of the response.
157
+
158
+ Args:
159
+ code (int): The status code to set.
160
+
161
+ Returns:
162
+ Response: The current Response object, allowing method chaining.
163
+ """
164
+ self.status(code)
165
+ return self
166
+
167
+ def set_headers(self, headers: Dict[str, str]) -> "Response":
168
+ """
169
+ Set custom headers for the response.
170
+
171
+ Args:
172
+ headers (Dict[str, str]): The headers to add or update.
173
+
174
+ Returns:
175
+ Response: The current Response object, allowing method chaining.
176
+ """
177
+ self.headers(headers)
178
+ return self
179
+
180
+ def set_body(self, body: Dict[str, Any]) -> "Response":
181
+ """
182
+ Update the body of the response with new data.
183
+
184
+ Args:
185
+ body (Dict[str, Any]): The body data to update.
186
+
187
+ Returns:
188
+ Response: The current Response object, allowing method chaining.
189
+ """
190
+ self.body.update(body)
191
+ return self
192
+
193
+ def exception(self) -> None:
194
+ """
195
+ Capture and format the current exception details and set a 500 status code.
196
+ Includes traceback information if DEBUG mode is enabled.
197
+ """
198
+ exc_type, exc_value, tb = sys.exc_info()
199
+ self.set_status(500)
200
+ self.set_body(
201
+ {
202
+ "python_exception": {
203
+ "type": str(exc_type),
204
+ "value": str(exc_value),
205
+ "traceback": traceback.format_exc() if DEBUG else None,
206
+ "tb": traceback.format_tb(tb) if DEBUG else None,
207
+ }
208
+ }
209
+ )
210
+
211
+ def console(self, message: str, title: str = "Notification") -> "Response":
212
+ """
213
+ Add a console log action to the response.
214
+
215
+ Args:
216
+ message (str): The console message.
217
+ title (str): Title for the console message. Defaults to "Notification".
218
+
219
+ Returns:
220
+ Response: The current Response object, allowing method chaining.
221
+ """
222
+ self.actions.append(
223
+ {
224
+ "action": "console",
225
+ "payload": {"title": title, "message": message},
226
+ }
227
+ )
228
+ return self
229
+
230
+ def redirect(self, location: str) -> "Response":
231
+ """
232
+ Add a redirect action to the response with the target location.
233
+
234
+ Args:
235
+ location (str): The URL to redirect to.
236
+
237
+ Returns:
238
+ Response: The current Response object, allowing method chaining.
239
+ """
240
+ self.actions.append({"action": "redirect", "payload": {"location": location}})
241
+ return self
242
+
243
+ def signout(self) -> "Response":
244
+ """
245
+ Add a signout action to the response.
246
+
247
+ Returns:
248
+ Response: The current Response object, allowing method chaining.
249
+ """
250
+ self.actions.append({"action": "signout"})
251
+ return self
252
+
253
+ def set_table(self, payload: Dict[str, Any]) -> "Response":
254
+ """
255
+ Add a set-table action to the response with the specified payload.
256
+
257
+ Args:
258
+ payload (Dict[str, Any]): The table data to set.
259
+
260
+ Returns:
261
+ Response: The current Response object, allowing method chaining.
262
+ """
263
+ self.actions.append({"action": "set-table", "payload": payload})
264
+ return self
265
+
266
+ def set_repo(self, payload: Dict[str, Any]) -> "Response":
267
+ """
268
+ Add a set-repo action to the response with the specified payload.
269
+
270
+ Args:
271
+ payload (Dict[str, Any]): The repository data to set.
272
+
273
+ Returns:
274
+ Response: The current Response object, allowing method chaining.
275
+ """
276
+ self.actions.append({"action": "set-repo", "payload": payload})
277
+ return self
278
+
279
+ @staticmethod
280
+ def _format_header_key(key: str) -> str:
281
+ """
282
+ Format HTTP headers to be in a title-cased format.
283
+
284
+ Args:
285
+ key (str): The header key to format.
286
+
287
+ Returns:
288
+ str: The formatted header key.
289
+ """
290
+ return "-".join(word.capitalize() for word in key.split("-"))
@@ -0,0 +1,185 @@
1
+ # iconv.py
2
+ import re
3
+ import codecs
4
+ from decimal import Decimal, ROUND_HALF_UP
5
+ from email.utils import parseaddr
6
+ from datetime import datetime
7
+ from typing import Optional, Union, Callable
8
+
9
+ # Convert data to SQL format for storage
10
+
11
+
12
+ def none(data: str) -> Optional[str]:
13
+ """Converts various 'null' representations to None."""
14
+ return None if data in ("", "null", "None", "@NULL") else data
15
+
16
+
17
+ def phone(data: str) -> Optional[str]:
18
+ """Extracts a 10-digit phone number or returns None if invalid."""
19
+ if data in ("None", None):
20
+ return None
21
+ cleaned_data = re.sub(r"[^0-9]", "", data)
22
+ match = re.search(r"\d{10}$", cleaned_data)
23
+ return match.group() if match else None
24
+
25
+
26
+ def day_of_week(data: str) -> Optional[int]:
27
+ """Converts day of the week to an integer representation."""
28
+ if not data:
29
+ return None
30
+ days = {
31
+ "monday": 1,
32
+ "tuesday": 2,
33
+ "wednesday": 3,
34
+ "thursday": 4,
35
+ "friday": 5,
36
+ "saturday": 6,
37
+ "sunday": 7,
38
+ "mon": 1,
39
+ "tue": 2,
40
+ "wed": 3,
41
+ "thu": 4,
42
+ "fri": 5,
43
+ "sat": 6,
44
+ "sun": 7,
45
+ }
46
+ return days.get(data.lower())
47
+
48
+
49
+ def date(data: str, fmt: str = "%Y-%m-%d") -> Optional[datetime.date]:
50
+ """Parses a date string into a date object using the specified format."""
51
+ try:
52
+ return datetime.strptime(data, fmt).date()
53
+ except (ValueError, TypeError):
54
+ return None
55
+
56
+
57
+ def time(data: str, fmt: str = "%X") -> Optional[datetime.time]:
58
+ """Parses a time string into a time object using the specified format."""
59
+ try:
60
+ return datetime.strptime(data, fmt).time()
61
+ except (ValueError, TypeError):
62
+ return None
63
+
64
+
65
+ def timestamp(data: str, fmt: str = "%c") -> Optional[datetime]:
66
+ """Parses a timestamp string into a datetime object using the specified format."""
67
+ try:
68
+ return datetime.strptime(data, fmt)
69
+ except (ValueError, TypeError):
70
+ return None
71
+
72
+
73
+ def email(data: str) -> Optional[str]:
74
+ """Validates and returns an email address if properly formatted."""
75
+ if not data or data.lower() == "none":
76
+ return None
77
+ data = data.strip().lower()
78
+ email_address = parseaddr(data)[1]
79
+ if "@" in email_address and "." in email_address.split("@")[1]:
80
+ return email_address
81
+ raise ValueError("Invalid email format")
82
+
83
+
84
+ def integer(data: str) -> int:
85
+ """Converts a string to an integer, removing non-numeric characters."""
86
+ return int(re.sub(r"[^0-9\.-]", "", data))
87
+
88
+
89
+ def boolean(data: Union[str, bool]) -> bool:
90
+ """Converts various string representations to a boolean."""
91
+ if isinstance(data, str) and data.lower() in ["false", "", "f", "off", "no"]:
92
+ return False
93
+ return bool(data)
94
+
95
+
96
+ def rot13(data: str) -> str:
97
+ """Encodes a string using ROT13."""
98
+ return codecs.encode(data, "rot13")
99
+
100
+
101
+ def pointer(data: Union[str, None]) -> Optional[int]:
102
+ """Converts a pointer to an integer, or returns None for null values."""
103
+ if data in ("@new", "", "@NULL", None):
104
+ return None
105
+ return int(data)
106
+
107
+
108
+ def money(data: str) -> Optional[Decimal]:
109
+ """Converts a monetary string to a Decimal, removing non-numeric characters."""
110
+ if data in ("None", None):
111
+ return None
112
+ return Decimal(re.sub(r"[^0-9\.-]", "", data))
113
+
114
+
115
+ def round_to(
116
+ precision: int, data: Optional[Union[str, float, Decimal]] = None
117
+ ) -> Union[Decimal, Callable[[Union[str, float, Decimal]], Decimal]]:
118
+ """Rounds a number to a specified precision."""
119
+
120
+ def function(value):
121
+ if value in ("None", None):
122
+ return None
123
+ if isinstance(value, str):
124
+ value = re.sub(r"[^0-9\.-]", "", value)
125
+ return Decimal(value).quantize(
126
+ Decimal(10) ** -precision, rounding=ROUND_HALF_UP
127
+ )
128
+
129
+ return function(data) if data is not None else function
130
+
131
+
132
+ def decimal(data: str) -> Optional[Decimal]:
133
+ """Converts a numeric string to a Decimal, removing non-numeric characters."""
134
+ if data in ("None", None):
135
+ return None
136
+ return Decimal(re.sub(r"[^0-9\.-]", "", data))
137
+
138
+
139
+ def ein(data: str) -> Optional[str]:
140
+ """Validates and returns a 9-digit EIN, or None if invalid."""
141
+ if data in ("None", None):
142
+ return None
143
+ cleaned_data = re.sub(r"[^0-9]", "", data)
144
+ match = re.fullmatch(r"\d{9}", cleaned_data)
145
+ return match.group() if match else None
146
+
147
+
148
+ def to_list(data: Union[str, list]) -> Optional[list]:
149
+ """Converts a string or single element into a list representation."""
150
+ if data in (None, "None"):
151
+ return None
152
+ if isinstance(data, str) and data.startswith("["):
153
+ return eval(data) # Assuming the input string is a list string
154
+ return [data] if not isinstance(data, list) else data
155
+
156
+
157
+ def title(data: str) -> str:
158
+ """Converts a string to title case."""
159
+ return "" if data in (None, "None") else str(data).title()
160
+
161
+
162
+ def lower(data: str) -> str:
163
+ """Converts a string to lowercase."""
164
+ return "" if data in (None, "None") else str(data).lower()
165
+
166
+
167
+ def upper(data: str) -> str:
168
+ """Converts a string to uppercase."""
169
+ return "" if data in (None, "None") else str(data).upper()
170
+
171
+
172
+ def padding(length: int, char: str = " ") -> Callable[[str], Optional[str]]:
173
+ """Pads a string to the specified length with a given character."""
174
+
175
+ def inner(data: str) -> Optional[str]:
176
+ if data in [None, "None", ""]:
177
+ return None
178
+ return str(data).rjust(length, char)
179
+
180
+ return inner
181
+
182
+
183
+ def string(data: str) -> Optional[str]:
184
+ """Converts an empty string to None, otherwise returns the string itself."""
185
+ return None if data == "" else str(data)
@@ -0,0 +1,170 @@
1
+ import re
2
+ import codecs
3
+ import decimal
4
+ from datetime import datetime, date, time
5
+ from pprint import pformat
6
+ from typing import Optional, Union, List, Callable
7
+
8
+ # Convert SQL data to JS format for display
9
+
10
+
11
+ def none(data: Optional[str]) -> str:
12
+ """Converts various 'null' representations to an empty string."""
13
+ return "" if data in (None, "None", "null", "@NULL") else data
14
+
15
+
16
+ def phone(data: Optional[str]) -> str:
17
+ """Formats a 10-digit phone number as (XXX) XXX-XXXX or returns an empty string if invalid."""
18
+ if data in (None, "None", ""):
19
+ return ""
20
+ digits = re.sub(r"[^0-9]", "", data)
21
+ match = re.search(r"\d{10}$", digits)
22
+ if match:
23
+ num = match.group()
24
+ return f"({num[:3]}) {num[3:6]}-{num[6:]}"
25
+ return ""
26
+
27
+
28
+ def day_of_week(data: Union[int, str, List], abbrev: bool = False) -> str:
29
+ """Converts a day number (1-7) to a day name, abbreviated if specified. Supports lists."""
30
+ days_full = {
31
+ 1: "Monday",
32
+ 2: "Tuesday",
33
+ 3: "Wednesday",
34
+ 4: "Thursday",
35
+ 5: "Friday",
36
+ 6: "Saturday",
37
+ 7: "Sunday",
38
+ }
39
+ days_abbrev = {1: "Mon", 2: "Tue", 3: "Wed", 4: "Thu", 5: "Fri", 6: "Sat", 7: "Sun"}
40
+ days = days_abbrev if abbrev else days_full
41
+
42
+ if isinstance(data, list):
43
+ return ",".join(day_of_week(day, abbrev) for day in data if day in days)
44
+
45
+ try:
46
+ return days[int(data)]
47
+ except (ValueError, KeyError, TypeError):
48
+ return ""
49
+
50
+
51
+ def date(data: Union[datetime, date, str], fmt: str = "%Y-%m-%d") -> str:
52
+ """Formats a date object as a string according to the specified format."""
53
+ return data.strftime(fmt) if isinstance(data, (datetime, date)) else str(data)
54
+
55
+
56
+ def time(data: Union[datetime, time, str], fmt: str = "%X") -> str:
57
+ """Formats a time object as a string according to the specified format."""
58
+ return data.strftime(fmt) if isinstance(data, (datetime, time)) else str(data)
59
+
60
+
61
+ def timestamp(data: Union[datetime, str], fmt: str = "%c") -> str:
62
+ """Formats a datetime object as a string according to the specified format."""
63
+ return data.strftime(fmt) if isinstance(data, datetime) else str(data)
64
+
65
+
66
+ def email(data: Optional[str]) -> str:
67
+ """Returns a lowercase email address or an empty string if invalid."""
68
+ return "" if data in (None, "None") else data.lower()
69
+
70
+
71
+ def pointer(data: Union[str, int]) -> Union[int, str]:
72
+ """Converts a string to an integer, or returns an empty string if conversion fails."""
73
+ try:
74
+ return int(data)
75
+ except (ValueError, TypeError):
76
+ return ""
77
+
78
+
79
+ def rot13(data: str) -> str:
80
+ """Encodes a string using ROT13."""
81
+ return codecs.decode(data, "rot13")
82
+
83
+
84
+ def boolean(data: Union[str, bool]) -> bool:
85
+ """Converts various representations to a boolean."""
86
+ if isinstance(data, str) and data.lower() in ["false", "", "f", "off", "no"]:
87
+ return False
88
+ return bool(data)
89
+
90
+
91
+ def money(data: str) -> str:
92
+ """Formats a numeric string as currency."""
93
+ if data in [None, ""]:
94
+ return ""
95
+ cleaned_data = re.sub(r"[^0-9\.-]", "", str(data))
96
+ return f"${decimal.Decimal(cleaned_data):,.2f}"
97
+
98
+
99
+ def round_to(
100
+ precision: int, data: Optional[Union[str, float, decimal.Decimal]] = None
101
+ ) -> Union[Callable, str]:
102
+ """Rounds a number to the specified precision."""
103
+
104
+ def function(value):
105
+ cleaned_value = re.sub(r"[^0-9\.-]", "", str(value))
106
+ return (
107
+ f"{decimal.Decimal(cleaned_value):.{precision}f}" if cleaned_value else "0"
108
+ )
109
+
110
+ return function if data is None else function(data)
111
+
112
+
113
+ def ein(data: str) -> str:
114
+ """Formats a 9-digit EIN as XX-XXXXXXX or returns an empty string if invalid."""
115
+ if data in (None, "None", ""):
116
+ return ""
117
+ cleaned_data = re.sub(r"[^0-9]", "", data)
118
+ match = re.fullmatch(r"\d{9}", cleaned_data)
119
+ return f"{cleaned_data[:2]}-{cleaned_data[2:]}" if match else ""
120
+
121
+
122
+ def to_list(data: Union[str, List]) -> Optional[List]:
123
+ """Converts a single element or JSON-like list string to a list."""
124
+ if data in (None, "None"):
125
+ return None
126
+ if isinstance(data, list):
127
+ return data
128
+ if isinstance(data, str) and data.startswith("["):
129
+ try:
130
+ return eval(data) # Be cautious with eval; only use if data is trusted
131
+ except (SyntaxError, NameError):
132
+ return None
133
+ return [data]
134
+
135
+
136
+ def title(data: Optional[str]) -> str:
137
+ """Converts a string to title case."""
138
+ return "" if data in (None, "None") else str(data).title()
139
+
140
+
141
+ def lower(data: Optional[str]) -> str:
142
+ """Converts a string to lowercase."""
143
+ return "" if data in (None, "None") else str(data).lower()
144
+
145
+
146
+ def upper(data: Optional[str]) -> str:
147
+ """Converts a string to uppercase."""
148
+ return "" if data in (None, "None") else str(data).upper()
149
+
150
+
151
+ def padding(length: int, char: str) -> Callable[[str], str]:
152
+ """Returns a function that pads a string to the specified length with the given character."""
153
+
154
+ def inner(data: str) -> str:
155
+ return str(data).rjust(length, char) if data not in (None, "None", "") else ""
156
+
157
+ return inner
158
+
159
+
160
+ def pprint(data: str) -> str:
161
+ """Pretty-prints a JSON-like string representation of data."""
162
+ try:
163
+ return pformat(eval(data))
164
+ except (SyntaxError, NameError):
165
+ return data
166
+
167
+
168
+ def string(data: Optional[str]) -> str:
169
+ """Converts a None value to an empty string; otherwise returns the string itself."""
170
+ return "" if data is None else str(data)