velocity-python 0.0.31__py3-none-any.whl → 0.0.33__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 velocity-python might be problematic. Click here for more details.

velocity/misc/export.py CHANGED
@@ -1,143 +1,146 @@
1
+ import decimal
2
+ import json
3
+ from datetime import datetime, date, time, timedelta
4
+ from typing import Union, List, Dict
5
+ from io import BytesIO
6
+ import base64
1
7
  import openpyxl
2
8
  from openpyxl.styles import NamedStyle, Font, Border, Side, Alignment
3
9
  from openpyxl.utils import get_column_letter
4
- from io import BytesIO
5
- import base64
6
10
 
7
11
 
8
- def extract(d, keys):
9
- return [d[key] for key in keys]
12
+ def extract(d: dict, keys: List[str]) -> List:
13
+ """Extract values from a dictionary based on a list of keys."""
14
+ return [d.get(key) for key in keys]
10
15
 
11
16
 
12
- def autosize_columns(ws, fixed={}):
13
- # Try to autosize the columns (doesn't always work due to dynamic
14
- # content, font family and font size differences, etc.) There is no
15
- # easy way to do this when buiding excel files.
17
+ def autosize_columns(ws, fixed: Dict[str, float] = {}):
18
+ """Autosize columns in the worksheet based on content length."""
16
19
  for col in ws.columns:
17
20
  max_length = 0
18
21
  for cell in col:
19
- try: # Necessary to avoid error on empty cells
20
- if len(str(cell.value)) > max_length:
21
- max_length = len(cell.value)
22
- except:
23
- pass
22
+ try:
23
+ if cell.value and len(str(cell.value)) > max_length:
24
+ max_length = len(str(cell.value))
25
+ except Exception:
26
+ continue
24
27
  adjusted_width = (max_length + 2) * 1.2
25
- try:
26
- l = get_column_letter(col[0].column)
27
- if l in fixed:
28
- adjusted_width = fixed["l"]
29
- ws.column_dimensions[l].width = adjusted_width
30
- except:
31
- l = col[0].column
32
- if l in fixed:
33
- adjusted_width = fixed["l"]
34
- ws.column_dimensions[l].width = adjusted_width
28
+ col_letter = get_column_letter(col[0].column)
29
+ ws.column_dimensions[col_letter].width = fixed.get(col_letter, adjusted_width)
35
30
 
36
31
 
37
32
  def create_spreadsheet(
38
- headers,
39
- rows,
33
+ headers: List[str],
34
+ rows: List[List],
40
35
  fileorbuffer,
41
- styles={},
42
- merge=[],
43
- formats={},
44
- named_styles=[],
45
- freeze_panes="A2",
46
- dimensions=None,
47
- auto_size=True,
36
+ styles: Dict[str, str] = {},
37
+ merge: List[str] = [],
38
+ formats: Dict[str, str] = {},
39
+ named_styles: List[NamedStyle] = [],
40
+ freeze_panes: str = "A2",
41
+ dimensions: dict = None,
42
+ auto_size: bool = True,
48
43
  ):
44
+ """Create an Excel spreadsheet with specified headers, rows, and styles."""
49
45
  wb = openpyxl.Workbook()
50
46
  ws = wb.active
51
47
 
52
- local_styles = {}
53
-
54
- style = NamedStyle(name="col_header")
55
- style.font = Font(bold=True)
56
- style.border = Border(bottom=Side(style="medium", color="000000"))
57
- local_styles[style.name] = style
58
-
59
- style = NamedStyle(name="sum_total")
60
- style.border = Border(bottom=Side(style="double", color="000000"))
61
- local_styles[style.name] = style
62
-
63
- style = NamedStyle(name="sub_total")
64
- style.font = Font(bold=True)
65
- style.border = Border(bottom=Side(style="thin", color="000000"))
66
- local_styles[style.name] = style
67
-
68
- style = NamedStyle(name="bold")
69
- style.font = Font(bold=True)
70
- local_styles[style.name] = style
71
-
72
- style = NamedStyle(name="align_right")
73
- style.font = Font(bold=True)
74
- style.border = Border(top=Side(style="thin", color="000000"))
75
- style.alignment = Alignment(horizontal="right", vertical="center")
76
- local_styles[style.name] = style
77
-
78
- style = NamedStyle(name="align_left")
79
- style.font = Font(bold=True)
80
- style.border = Border(top=Side(style="thin", color="000000"))
81
- style.alignment = Alignment(horizontal="left", vertical="center")
82
- local_styles[style.name] = style
83
-
84
- style = NamedStyle(name="align_right_double")
85
- style.font = Font(bold=True)
86
- style.border = Border(top=Side(style="double", color="000000"))
87
- style.alignment = Alignment(horizontal="right", vertical="center")
88
- local_styles[style.name] = style
89
-
90
- style = NamedStyle(name="align_left_double")
91
- style.font = Font(bold=True)
92
- style.border = Border(top=Side(style="double", color="000000"))
93
- style.alignment = Alignment(horizontal="left", vertical="center")
94
- local_styles[style.name] = style
95
-
48
+ # Define default named styles
49
+ def get_named_styles():
50
+ named_styles = {
51
+ "col_header": NamedStyle(
52
+ name="col_header",
53
+ font=Font(bold=True),
54
+ border=Border(bottom=Side(style="medium", color="000000")),
55
+ ),
56
+ "sum_total": NamedStyle(
57
+ name="sum_total",
58
+ border=Border(bottom=Side(style="double", color="000000")),
59
+ ),
60
+ "sub_total": NamedStyle(
61
+ name="sub_total",
62
+ font=Font(bold=True),
63
+ border=Border(bottom=Side(style="thin", color="000000")),
64
+ ),
65
+ "bold": NamedStyle(name="bold", font=Font(bold=True)),
66
+ "align_right": NamedStyle(
67
+ name="align_right",
68
+ font=Font(bold=True),
69
+ border=Border(top=Side(style="thin", color="000000")),
70
+ alignment=Alignment(horizontal="right", vertical="center"),
71
+ ),
72
+ "align_left": NamedStyle(
73
+ name="align_left",
74
+ font=Font(bold=True),
75
+ border=Border(top=Side(style="thin", color="000000")),
76
+ alignment=Alignment(horizontal="left", vertical="center"),
77
+ ),
78
+ "align_right_double": NamedStyle(
79
+ name="align_right_double",
80
+ font=Font(bold=True),
81
+ border=Border(top=Side(style="double", color="000000")),
82
+ alignment=Alignment(horizontal="right", vertical="center"),
83
+ ),
84
+ "align_left_double": NamedStyle(
85
+ name="align_left_double",
86
+ font=Font(bold=True),
87
+ border=Border(top=Side(style="double", color="000000")),
88
+ alignment=Alignment(horizontal="left", vertical="center"),
89
+ ),
90
+ }
91
+ return named_styles
92
+
93
+ # Add default and user-defined styles
94
+ local_styles = get_named_styles()
96
95
  for style in named_styles:
97
96
  local_styles[style.name] = style
98
-
99
97
  for style in local_styles.values():
100
98
  wb.add_named_style(style)
101
99
 
100
+ # Add headers and rows
102
101
  ws.append(headers)
102
+ for row in rows:
103
+ ws.append(row)
103
104
 
104
- [ws.append(row) for row in rows]
105
-
106
- if freeze_panes:
107
- ws.freeze_panes = freeze_panes
105
+ # Set freeze panes
106
+ ws.freeze_panes = freeze_panes
108
107
 
108
+ # Auto-size columns if enabled
109
109
  if auto_size:
110
- autosize_columns(ws)
110
+ autosize_columns(ws, fixed={})
111
111
 
112
+ # Set row and column dimensions if provided
112
113
  if dimensions:
113
114
  for key, val in dimensions.get("rows", {}).items():
114
115
  ws.row_dimensions[key].height = val
115
116
  for key, val in dimensions.get("columns", {}).items():
116
117
  ws.column_dimensions[key].width = val
117
118
 
118
- for cell, style in styles.items():
119
- ws[cell].style = style
120
-
119
+ # Apply cell styles, merges, and formats
120
+ for cell, style_name in styles.items():
121
+ if style_name in local_styles:
122
+ ws[cell].style = local_styles[style_name]
121
123
  for cell_range in merge:
122
124
  ws.merge_cells(cell_range)
125
+ for cell, format_code in formats.items():
126
+ ws[cell].number_format = format_code
123
127
 
124
- for cell, format in formats.items():
125
- ws[cell].number_format = format
126
-
128
+ # Save workbook to the provided file or buffer
127
129
  wb.save(fileorbuffer)
128
130
 
129
131
 
130
- def getDownloadableSpreadsheet(
131
- headers,
132
- rows,
133
- styles={},
134
- merge=[],
135
- formats={},
136
- named_styles=[],
137
- freeze_panes="A2",
138
- dimensions=None,
139
- auto_size=True,
140
- ):
132
+ def get_downloadable_spreadsheet(
133
+ headers: List[str],
134
+ rows: List[List],
135
+ styles: Dict[str, str] = {},
136
+ merge: List[str] = [],
137
+ formats: Dict[str, str] = {},
138
+ named_styles: List[NamedStyle] = [],
139
+ freeze_panes: str = "A2",
140
+ dimensions: dict = None,
141
+ auto_size: bool = True,
142
+ ) -> str:
143
+ """Generate a downloadable spreadsheet encoded in base64."""
141
144
  buffer = BytesIO()
142
145
  create_spreadsheet(
143
146
  headers,
velocity/misc/format.py CHANGED
@@ -1,47 +1,45 @@
1
1
  import decimal
2
2
  import json
3
3
  from datetime import datetime, date, time, timedelta
4
+ from typing import Union
4
5
 
5
6
 
6
- def gallons(data):
7
+ def gallons(data: Union[None, str, float, decimal.Decimal]) -> str:
8
+ """Converts a value to a string formatted in gallons with two decimal places."""
7
9
  if data is None:
8
10
  return ""
9
- data = decimal.Decimal(data)
10
- return "{:.2f}".format(data)
11
- # return "{:1,.2f} gals".format(data)
11
+ try:
12
+ data = decimal.Decimal(data)
13
+ return f"{data:.2f}"
14
+ except (decimal.InvalidOperation, ValueError, TypeError):
15
+ return "" # Return an empty string for invalid input
12
16
 
13
17
 
14
- def gallons2liters(data):
18
+ def gallons2liters(data: Union[None, str, float, decimal.Decimal]) -> str:
19
+ """Converts gallons to liters and formats with two decimal places."""
15
20
  if data is None:
16
21
  return ""
17
- data = decimal.Decimal(data) * decimal.Decimal(3.78541)
18
- return "{:.2f}".format(data)
19
- # return "{:1,.2f} liters".format(data)
22
+ try:
23
+ data = decimal.Decimal(data) * decimal.Decimal("3.78541")
24
+ return f"{data:.2f}"
25
+ except (decimal.InvalidOperation, ValueError, TypeError):
26
+ return ""
20
27
 
21
28
 
22
- def currency(data):
29
+ def currency(data: Union[None, str, float, decimal.Decimal]) -> str:
30
+ """Formats a value as currency with two decimal places."""
23
31
  if data is None:
24
32
  return ""
25
- decimal.Decimal(data)
26
- return "{:.2f}".format(data)
27
- # return "${:1,.2f}".format(data)
33
+ try:
34
+ data = decimal.Decimal(data)
35
+ return f"{data:.2f}"
36
+ except (decimal.InvalidOperation, ValueError, TypeError):
37
+ return ""
28
38
 
29
39
 
30
- def human_delta(tdelta):
31
- """
32
- Takes a timedelta object and formats it for humans.
33
- Usage:
34
- # 149 day(s) 8 hr(s) 36 min 19 sec
35
- print(human_delta(datetime(2014, 3, 30) - datetime.now()))
36
- Example Results:
37
- 23 sec
38
- 12 min 45 sec
39
- 1 hr(s) 11 min 2 sec
40
- 3 day(s) 13 hr(s) 56 min 34 sec
41
- :param tdelta: The timedelta object.
42
- :return: The human formatted timedelta
43
- """
44
- d = dict(days=tdelta.days)
40
+ def human_delta(tdelta: timedelta) -> str:
41
+ """Formats a timedelta object into a human-readable format."""
42
+ d = {"days": tdelta.days, "hrs": 0, "min": 0, "sec": 0}
45
43
  d["hrs"], rem = divmod(tdelta.seconds, 3600)
46
44
  d["min"], d["sec"] = divmod(rem, 60)
47
45
 
@@ -57,26 +55,25 @@ def human_delta(tdelta):
57
55
  return fmt.format(**d)
58
56
 
59
57
 
60
- def to_json(o, datefmt="%Y-%m-%d", timefmt="%H:%M:%S"):
58
+ def to_json(o, datefmt: str = "%Y-%m-%d", timefmt: str = "%H:%M:%S") -> str:
59
+ """Serializes an object to JSON, handling special types like Decimal and datetime."""
60
+
61
61
  class JsonEncoder(json.JSONEncoder):
62
- def default(self, o):
63
- if hasattr(o, "to_json"):
64
- return o.to_json()
65
- elif o is None:
66
- return 0
67
- elif isinstance(o, decimal.Decimal):
68
- return float(o)
69
- elif isinstance(o, date):
70
- return o.strftime(datefmt)
71
- elif isinstance(o, datetime):
72
- return o.strftime("{} {}".format(datefmt, timefmt))
73
- elif isinstance(o, time):
74
- return o.strftime(timefmt)
75
- elif isinstance(o, timedelta):
76
- return human_delta(o)
77
- try:
78
- return super(JsonEncoder, self).default(o)
79
- except:
80
- return str(o)
62
+ def default(self, obj):
63
+ if hasattr(obj, "to_json"):
64
+ return obj.to_json()
65
+ elif obj is None:
66
+ return None
67
+ elif isinstance(obj, decimal.Decimal):
68
+ return float(obj)
69
+ elif isinstance(obj, date):
70
+ return obj.strftime(datefmt)
71
+ elif isinstance(obj, datetime):
72
+ return obj.strftime(f"{datefmt} {timefmt}")
73
+ elif isinstance(obj, time):
74
+ return obj.strftime(timefmt)
75
+ elif isinstance(obj, timedelta):
76
+ return human_delta(obj)
77
+ return super().default(obj)
81
78
 
82
79
  return json.dumps(o, cls=JsonEncoder)
velocity/misc/mail.py CHANGED
@@ -1,74 +1,78 @@
1
1
  #!/usr/local/bin/python
2
2
  from email.parser import Parser as EmailParser
3
- from io import BytesIO
4
- import mimetypes, hashlib
3
+ from email.message import Message
4
+ from typing import List, Optional
5
+ import mimetypes
6
+ import hashlib
5
7
 
6
8
 
7
9
  class NotSupportedMailFormat(Exception):
10
+ """Exception raised for unsupported mail formats."""
11
+
8
12
  pass
9
13
 
10
14
 
11
- def get_full_emails(addresses):
15
+ class Attachment:
16
+ """Represents an email attachment."""
17
+
18
+ def __init__(self, name: str, data: bytes):
19
+ self.name = name
20
+ self.data = data
21
+ self.ctype = mimetypes.guess_type(name)[0] or "application/octet-stream"
22
+ self.size = len(data)
23
+ self.hash = hashlib.sha1(data).hexdigest()
24
+
25
+
26
+ def get_full_emails(addresses: List) -> List[str]:
27
+ """Generates a list of formatted email addresses with names."""
12
28
  results = []
13
29
  for a in addresses:
14
- if a.name:
15
- results.append(
16
- f"{a.name.decode('utf-8')} <{a.mailbox.decode('utf-8')}@{a.host.decode('utf-8')}>"
17
- )
30
+ mailbox = a.mailbox.decode("utf-8")
31
+ host = a.host.decode("utf-8")
32
+ name = a.name.decode("utf-8") if a.name else None
33
+ if name:
34
+ results.append(f"{name} <{mailbox}@{host}>")
18
35
  else:
19
- results.append(f"{a.mailbox.decode('utf-8')}@{a.host.decode('utf-8')}")
36
+ results.append(f"{mailbox}@{host}")
20
37
  return results
21
38
 
22
39
 
23
- def get_address_only(addresses):
24
- results = []
25
- for a in addresses:
26
- results.append(f"{a.mailbox.decode('utf-8')}@{a.host.decode('utf-8')}")
27
- return results
40
+ def get_address_only(addresses: List) -> List[str]:
41
+ """Generates a list of email addresses without names."""
42
+ return [f"{a.mailbox.decode('utf-8')}@{a.host.decode('utf-8')}" for a in addresses]
28
43
 
29
44
 
30
- def parse_attachment(part):
45
+ def parse_attachment(part: Message) -> Optional[Attachment]:
46
+ """Parses an attachment from a message part if present."""
31
47
  content_disposition = part.get("Content-Disposition")
32
48
  if content_disposition:
33
49
  dispositions = content_disposition.strip().split(";")
34
- if content_disposition and dispositions[0].lower() == "attachment":
50
+ if dispositions[0].lower() == "attachment":
35
51
  name = part.get_filename()
36
- if not name:
37
- return None
38
52
  data = part.get_payload(decode=True)
39
- if not data:
40
- return None
41
- attachment = Object()
42
- attachment.data = data
43
- attachment.ctype = mimetypes.guess_type(name)[0]
44
- attachment.size = len(data)
45
- attachment.name = name
46
- attachment.hash = hashlib.sha1(data).hexdigest()
47
-
48
- return attachment
49
-
53
+ if name and data:
54
+ return Attachment(name=name, data=data)
50
55
  return None
51
56
 
52
57
 
53
- def parse(content):
54
- body = None
55
- html = None
58
+ def parse(content: str) -> dict:
59
+ """Parses the email content and extracts plain text, HTML, and attachments."""
60
+ body = bytearray()
61
+ html = bytearray()
56
62
  attachments = []
57
- for part in EmailParser().parsestr(content).walk():
63
+
64
+ message = EmailParser().parsestr(content)
65
+ for part in message.walk():
58
66
  attachment = parse_attachment(part)
59
67
  if attachment:
60
68
  attachments.append(attachment)
61
69
  elif part.get_content_type() == "text/plain":
62
- if body is None:
63
- body = bytes()
64
- body += part.get_payload(decode=True)
70
+ body.extend(part.get_payload(decode=True) or b"")
65
71
  elif part.get_content_type() == "text/html":
66
- if html is None:
67
- html = bytes()
68
- html += part.get_payload(decode=True)
72
+ html.extend(part.get_payload(decode=True) or b"")
69
73
 
70
74
  return {
71
- "body": body,
72
- "html": html,
75
+ "body": body.decode("utf-8") if body else None,
76
+ "html": html.decode("utf-8") if html else None,
73
77
  "attachments": attachments,
74
78
  }
velocity/misc/merge.py CHANGED
@@ -1,36 +1,52 @@
1
1
  from copy import deepcopy
2
2
  from functools import reduce
3
+ from typing import Dict, List, Any
3
4
 
4
5
 
5
- def deep_merge(*dicts, update=False):
6
+ def deep_merge(*dicts: Dict[str, Any], update: bool = False) -> Dict[str, Any]:
6
7
  """
7
- Merges dicts deeply.
8
+ Deeply merges multiple dictionaries.
9
+
8
10
  Parameters
9
11
  ----------
10
- dicts : list[dict]
11
- List of dicts.
12
- update : bool
13
- Whether to update the first dict or create a new dict.
12
+ *dicts : Dict[str, Any]
13
+ Variable number of dictionaries to merge.
14
+ update : bool, optional
15
+ If True, updates the first dictionary in-place.
16
+ If False, creates and returns a new merged dictionary. Default is False.
17
+
14
18
  Returns
15
19
  -------
16
- merged : dict
17
- Merged dict.
20
+ Dict[str, Any]
21
+ The merged dictionary.
22
+
23
+ Notes
24
+ -----
25
+ - If a key's value in two dictionaries is a dictionary, they are merged recursively.
26
+ - If a key's value in two dictionaries is a list, values from the second list
27
+ are added to the first, avoiding duplicates.
28
+ - For all other types, the value from the latter dictionary overwrites the former.
18
29
  """
19
30
 
20
- def merge_into(d1, d2):
21
- for key in d2:
31
+ def merge_into(d1: Dict[str, Any], d2: Dict[str, Any]) -> Dict[str, Any]:
32
+ for key, value in d2.items():
22
33
  if key not in d1:
23
- d1[key] = deepcopy(d2[key])
24
- elif isinstance(d1[key], dict):
25
- d1[key] = merge_into(d1[key], d2[key])
26
- elif isinstance(d1[key], list) and isinstance(d2[key], list):
27
- d1[key].extend(d2[key])
34
+ d1[key] = deepcopy(value)
35
+ elif isinstance(d1[key], dict) and isinstance(value, dict):
36
+ d1[key] = merge_into(d1[key], value)
37
+ elif isinstance(d1[key], list) and isinstance(value, list):
38
+ existing_items = set(d1[key])
39
+ d1[key].extend(x for x in value if x not in existing_items)
40
+ existing_items.update(
41
+ value
42
+ ) # Keep the set updated for further iterations
28
43
  else:
29
- d1[key] = deepcopy(d2[key])
30
-
44
+ d1[key] = deepcopy(value) # Overwrite with the new value
31
45
  return d1
32
46
 
33
47
  if update:
48
+ # Update the first dictionary in-place
34
49
  return reduce(merge_into, dicts[1:], dicts[0])
35
50
  else:
51
+ # Create a new dictionary for the merged result
36
52
  return reduce(merge_into, dicts, {})
velocity/misc/timer.py CHANGED
@@ -1,27 +1,50 @@
1
1
  import time
2
+ from typing import Optional
2
3
 
3
4
 
4
5
  class Timer:
5
- def __init__(self, label="Timer"):
6
+ def __init__(self, label: str = "Timer"):
7
+ """
8
+ Initializes a Timer instance with an optional label and starts the timer.
9
+ """
6
10
  self._label = label
11
+ self._start: Optional[float] = None
12
+ self._end: Optional[float] = None
13
+ self._diff: Optional[float] = None
7
14
  self.start()
8
15
 
9
- def start(self):
10
- self._end = self._start = time.time()
11
- self._diff = 0
16
+ def start(self) -> None:
17
+ """Starts or restarts the timer."""
18
+ self._start = time.time()
19
+ self._end = None
20
+ self._diff = None
12
21
 
13
- def end(self):
22
+ def stop(self) -> float:
23
+ """Stops the timer and calculates the time elapsed."""
24
+ if self._start is None:
25
+ raise ValueError("Timer has not been started.")
14
26
  self._end = time.time()
15
27
  self._diff = self._end - self._start
28
+ return self._diff
16
29
 
17
- def __str__(self):
18
- if not self._diff:
19
- self.end()
20
- return f"{self._label}: {self._diff:.4f} s"
30
+ def elapsed(self) -> float:
31
+ """Returns the elapsed time in seconds without stopping the timer."""
32
+ if self._start is None:
33
+ raise ValueError("Timer has not been started.")
34
+ return time.time() - self._start
35
+
36
+ def __str__(self) -> str:
37
+ """Returns a string representation of the time elapsed or final time."""
38
+ if self._diff is not None: # Timer has been stopped
39
+ return f"{self._label}: {self._diff:.4f} s"
40
+ else: # Timer is still running, show elapsed time
41
+ return f"{self._label}: {self.elapsed():.4f} s"
21
42
 
22
43
 
23
44
  if __name__ == "__main__":
24
45
  t = Timer("My Label")
25
46
  time.sleep(0.003)
47
+ print(t) # Should display elapsed time
26
48
  time.sleep(3)
27
- print(t)
49
+ t.stop()
50
+ print(t) # Should display the stopped time (final diff)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: velocity-python
3
- Version: 0.0.31
3
+ Version: 0.0.33
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