velocity-python 0.0.30__py3-none-any.whl → 0.0.32__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/db.py CHANGED
@@ -4,13 +4,14 @@ import string
4
4
  from functools import wraps
5
5
  from velocity.db import exceptions
6
6
 
7
- #TBD implement or delete all this code. It is not used anywhere.
7
+
8
+ # TBD implement or delete all this code. It is not used anywhere.
8
9
  def NotSupported(*args, **kwds):
9
10
  raise Exception("Sorry, the driver for this database is not installed")
10
11
 
11
12
 
12
13
  def NOTNULL(x):
13
- """ Helper function to filter out NULL values from keys/values functions """
14
+ """Helper function to filter out NULL values from keys/values functions"""
14
15
  return len(x) == 2 and x[1] is not None
15
16
 
16
17
 
@@ -23,24 +24,23 @@ def pipe(func, primary, secondary, *args, **kwds):
23
24
  class join(object):
24
25
  @classmethod
25
26
  def _or(cls, *args, **kwargs):
26
- return '(' + ' or '.join(cls._list(*args, **kwargs)) + ')'
27
+ return "(" + " or ".join(cls._list(*args, **kwargs)) + ")"
27
28
 
28
29
  @classmethod
29
30
  def _and(cls, *args, **kwargs):
30
- return '(' + ' and '.join(cls._list(*args, **kwargs)) + ')'
31
+ return "(" + " and ".join(cls._list(*args, **kwargs)) + ")"
31
32
 
32
33
  @classmethod
33
34
  def _list(cls, *args, **kwargs):
34
35
  vals = []
35
36
  vals.extend(args)
36
- for key,val in kwargs.items():
37
+ for key, val in kwargs.items():
37
38
  if isinstance(val, numbers.Number):
38
- vals.append("{}={}".format(key,val))
39
+ vals.append("{}={}".format(key, val))
39
40
  else:
40
- vals.append("{}='{}'".format(key,val))
41
+ vals.append("{}='{}'".format(key, val))
41
42
  return vals
42
43
 
43
-
44
44
 
45
45
  def return_default(default=None):
46
46
  """
@@ -48,38 +48,44 @@ def return_default(default=None):
48
48
  If an exception is raised within the function, the decorator
49
49
  catches the exception and returns the default value instead.
50
50
  """
51
+
51
52
  def decorator(f):
52
53
  f.default = default
54
+
53
55
  @wraps(f)
54
56
  def return_default(self, *args, **kwds):
55
57
  sp = self.tx.create_savepoint(cursor=self.table.cursor)
56
58
  try:
57
59
  result = f(self, *args, **kwds)
58
- except (exceptions.DbApplicationError,
59
- exceptions.DbTableMissingError,
60
- exceptions.DbColumnMissingError,
61
- exceptions.DbTruncationError,
62
- StopIteration,
63
- exceptions.DbObjectExistsError):
60
+ except (
61
+ exceptions.DbApplicationError,
62
+ exceptions.DbTableMissingError,
63
+ exceptions.DbColumnMissingError,
64
+ exceptions.DbTruncationError,
65
+ StopIteration,
66
+ exceptions.DbObjectExistsError,
67
+ ):
64
68
  self.tx.rollback_savepoint(sp, cursor=self.table.cursor)
65
69
  return f.default
66
70
  self.tx.release_savepoint(sp, cursor=self.table.cursor)
67
71
  return result
72
+
68
73
  return return_default
74
+
69
75
  return decorator
70
76
 
71
77
 
72
78
  def randomword(length=None):
73
79
  """
74
80
  Generate a random word consisting of lowercase letters. This is used to generate random savepoint names.
75
- The length of the word can be specified, otherwise a random length between 5 and 15 will be used.
76
-
81
+ The length of the word can be specified, otherwise a random length between 5 and 15 will be used.
82
+
77
83
  Parameters:
78
84
  length (int, optional): The length of the random word. If not provided, a random length between 5 and 15 will be used.
79
-
85
+
80
86
  Returns:
81
87
  str: The randomly generated word.
82
88
  """
83
89
  if length is None:
84
- length = random.randint(5,15)
85
- return ''.join(random.choice(string.ascii_lowercase) for i in range(length))
90
+ length = random.randint(5, 15)
91
+ return "".join(random.choice(string.ascii_lowercase) for i in range(length))
velocity/misc/export.py CHANGED
@@ -1,147 +1,157 @@
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
- def extract(d, keys):
8
- return [d[key] for key in keys]
11
+
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]
9
15
 
10
16
 
11
- def autosize_columns(ws, fixed={}):
12
- # Try to autosize the columns (doesn't always work due to dynamic
13
- # content, font family and font size differences, etc.) There is no
14
- # 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."""
15
19
  for col in ws.columns:
16
20
  max_length = 0
17
21
  for cell in col:
18
- try: # Necessary to avoid error on empty cells
19
- if len(str(cell.value)) > max_length:
20
- max_length = len(cell.value)
21
- except:
22
- 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
23
27
  adjusted_width = (max_length + 2) * 1.2
24
- try:
25
- l = get_column_letter(col[0].column)
26
- if l in fixed:
27
- adjusted_width = fixed['l']
28
- ws.column_dimensions[l].width = adjusted_width
29
- except:
30
- l = col[0].column
31
- if l in fixed:
32
- adjusted_width = fixed['l']
33
- ws.column_dimensions[l].width = adjusted_width
34
-
35
-
36
- def create_spreadsheet(headers,
37
- rows,
38
- fileorbuffer,
39
- styles={},
40
- merge=[],
41
- formats={},
42
- named_styles=[],
43
- freeze_panes='A2',
44
- dimensions=None,
45
- auto_size=True):
28
+ col_letter = get_column_letter(col[0].column)
29
+ ws.column_dimensions[col_letter].width = fixed.get(col_letter, adjusted_width)
30
+
31
+
32
+ def create_spreadsheet(
33
+ headers: List[str],
34
+ rows: List[List],
35
+ fileorbuffer,
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,
43
+ ):
44
+ """Create an Excel spreadsheet with specified headers, rows, and styles."""
46
45
  wb = openpyxl.Workbook()
47
46
  ws = wb.active
48
47
 
49
- local_styles = {}
50
-
51
- style = NamedStyle(name="col_header")
52
- style.font = Font(bold=True)
53
- style.border = Border(bottom=Side(style='medium', color="000000"))
54
- local_styles[style.name] = style
55
-
56
- style = NamedStyle(name="sum_total")
57
- style.border = Border(bottom=Side(style='double', color="000000"))
58
- local_styles[style.name] = style
59
-
60
- style = NamedStyle(name="sub_total")
61
- style.font = Font(bold=True)
62
- style.border = Border(bottom=Side(style='thin', color="000000"))
63
- local_styles[style.name] = style
64
-
65
- style = NamedStyle(name="bold")
66
- style.font = Font(bold=True)
67
- local_styles[style.name] = style
68
-
69
- style = NamedStyle(name="align_right")
70
- style.font = Font(bold=True)
71
- style.border = Border(top=Side(style='thin', color="000000"))
72
- style.alignment = Alignment(horizontal="right", vertical="center")
73
- local_styles[style.name] = style
74
-
75
- style = NamedStyle(name="align_left")
76
- style.font = Font(bold=True)
77
- style.border = Border(top=Side(style='thin', color="000000"))
78
- style.alignment = Alignment(horizontal="left", vertical="center")
79
- local_styles[style.name] = style
80
-
81
- style = NamedStyle(name="align_right_double")
82
- style.font = Font(bold=True)
83
- style.border = Border(top=Side(style='double', color="000000"))
84
- style.alignment = Alignment(horizontal="right", vertical="center")
85
- local_styles[style.name] = style
86
-
87
- style = NamedStyle(name="align_left_double")
88
- style.font = Font(bold=True)
89
- style.border = Border(top=Side(style='double', color="000000"))
90
- style.alignment = Alignment(horizontal="left", vertical="center")
91
- local_styles[style.name] = style
92
-
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()
93
95
  for style in named_styles:
94
96
  local_styles[style.name] = style
95
-
96
97
  for style in local_styles.values():
97
98
  wb.add_named_style(style)
98
99
 
100
+ # Add headers and rows
99
101
  ws.append(headers)
102
+ for row in rows:
103
+ ws.append(row)
100
104
 
101
- [ws.append(row) for row in rows]
102
-
103
- if freeze_panes:
104
- ws.freeze_panes = freeze_panes
105
+ # Set freeze panes
106
+ ws.freeze_panes = freeze_panes
105
107
 
108
+ # Auto-size columns if enabled
106
109
  if auto_size:
107
- autosize_columns(ws)
110
+ autosize_columns(ws, fixed={})
108
111
 
112
+ # Set row and column dimensions if provided
109
113
  if dimensions:
110
- for key, val in dimensions.get('rows', {}).items():
114
+ for key, val in dimensions.get("rows", {}).items():
111
115
  ws.row_dimensions[key].height = val
112
- for key, val in dimensions.get('columns', {}).items():
116
+ for key, val in dimensions.get("columns", {}).items():
113
117
  ws.column_dimensions[key].width = val
114
118
 
115
- for cell, style in styles.items():
116
- ws[cell].style = style
117
-
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]
118
123
  for cell_range in merge:
119
124
  ws.merge_cells(cell_range)
125
+ for cell, format_code in formats.items():
126
+ ws[cell].number_format = format_code
120
127
 
121
- for cell, format in formats.items():
122
- ws[cell].number_format = format
123
-
128
+ # Save workbook to the provided file or buffer
124
129
  wb.save(fileorbuffer)
125
-
126
-
127
- def getDownloadableSpreadsheet(headers,
128
- rows,
129
- styles={},
130
- merge=[],
131
- formats={},
132
- named_styles=[],
133
- freeze_panes='A2',
134
- dimensions=None,
135
- auto_size=True):
136
- buffer = BytesIO()
137
- create_spreadsheet(headers,
138
- rows,
139
- buffer,
140
- styles,
141
- merge,
142
- formats,
143
- named_styles,
144
- freeze_panes,
145
- dimensions,
146
- auto_size)
130
+
131
+
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."""
144
+ buffer = BytesIO()
145
+ create_spreadsheet(
146
+ headers,
147
+ rows,
148
+ buffer,
149
+ styles,
150
+ merge,
151
+ formats,
152
+ named_styles,
153
+ freeze_panes,
154
+ dimensions,
155
+ auto_size,
156
+ )
147
157
  return base64.b64encode(buffer.getvalue()).decode()
velocity/misc/format.py CHANGED
@@ -1,81 +1,79 @@
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
- return ''
9
- data = decimal.Decimal(data)
10
- return "{:.2f}".format(data)
11
- # return "{:1,.2f} gals".format(data)
10
+ return ""
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
- return ''
17
- data = decimal.Decimal(data) * decimal.Decimal(3.78541)
18
- return "{:.2f}".format(data)
19
- # return "{:1,.2f} liters".format(data)
21
+ return ""
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
- return ''
25
- decimal.Decimal(data)
26
- return "{:.2f}".format(data)
27
- # return "${:1,.2f}".format(data)
32
+ return ""
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)
45
- d['hrs'], rem = divmod(tdelta.seconds, 3600)
46
- d['min'], d['sec'] = divmod(rem, 60)
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}
43
+ d["hrs"], rem = divmod(tdelta.seconds, 3600)
44
+ d["min"], d["sec"] = divmod(rem, 60)
47
45
 
48
- if d['min'] == 0:
49
- fmt = '{sec} sec'
50
- elif d['hrs'] == 0:
51
- fmt = '{min} min {sec} sec'
52
- elif d['days'] == 0:
53
- fmt = '{hrs} hr(s) {min} min {sec} sec'
46
+ if d["min"] == 0:
47
+ fmt = "{sec} sec"
48
+ elif d["hrs"] == 0:
49
+ fmt = "{min} min {sec} sec"
50
+ elif d["days"] == 0:
51
+ fmt = "{hrs} hr(s) {min} min {sec} sec"
54
52
  else:
55
- fmt = '{days} day(s) {hrs} hr(s) {min} min {sec} sec'
53
+ fmt = "{days} day(s) {hrs} hr(s) {min} min {sec} sec"
56
54
 
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)
78
+
81
79
  return json.dumps(o, cls=JsonEncoder)
velocity/misc/mail.py CHANGED
@@ -1,67 +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
7
+
5
8
 
6
9
  class NotSupportedMailFormat(Exception):
10
+ """Exception raised for unsupported mail formats."""
11
+
7
12
  pass
8
13
 
9
- def get_full_emails(addresses):
14
+
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."""
10
28
  results = []
11
29
  for a in addresses:
12
- if a.name:
13
- results.append(f"{a.name.decode('utf-8')} <{a.mailbox.decode('utf-8')}@{a.host.decode('utf-8')}>")
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}>")
14
35
  else:
15
- results.append(f"{a.mailbox.decode('utf-8')}@{a.host.decode('utf-8')}")
36
+ results.append(f"{mailbox}@{host}")
16
37
  return results
17
38
 
18
- def get_address_only(addresses):
19
- results = []
20
- for a in addresses:
21
- results.append(f"{a.mailbox.decode('utf-8')}@{a.host.decode('utf-8')}")
22
- return results
23
39
 
24
- def parse_attachment(part):
25
- content_disposition = part.get('Content-Disposition')
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]
43
+
44
+
45
+ def parse_attachment(part: Message) -> Optional[Attachment]:
46
+ """Parses an attachment from a message part if present."""
47
+ content_disposition = part.get("Content-Disposition")
26
48
  if content_disposition:
27
49
  dispositions = content_disposition.strip().split(";")
28
- if content_disposition and dispositions[0].lower() == "attachment":
50
+ if dispositions[0].lower() == "attachment":
29
51
  name = part.get_filename()
30
- if not name:
31
- return None
32
52
  data = part.get_payload(decode=True)
33
- if not data:
34
- return None
35
- attachment = Object()
36
- attachment.data = data
37
- attachment.ctype = mimetypes.guess_type(name)[0]
38
- attachment.size = len(data)
39
- attachment.name = name
40
- attachment.hash = hashlib.sha1(data).hexdigest()
41
-
42
- return attachment
43
-
53
+ if name and data:
54
+ return Attachment(name=name, data=data)
44
55
  return None
45
56
 
46
- def parse(content):
47
- body = None
48
- html = None
57
+
58
+ def parse(content: str) -> dict:
59
+ """Parses the email content and extracts plain text, HTML, and attachments."""
60
+ body = bytearray()
61
+ html = bytearray()
49
62
  attachments = []
50
- for part in EmailParser().parsestr(content).walk():
63
+
64
+ message = EmailParser().parsestr(content)
65
+ for part in message.walk():
51
66
  attachment = parse_attachment(part)
52
67
  if attachment:
53
68
  attachments.append(attachment)
54
69
  elif part.get_content_type() == "text/plain":
55
- if body is None:
56
- body = bytes()
57
- body += part.get_payload(decode=True)
70
+ body.extend(part.get_payload(decode=True) or b"")
58
71
  elif part.get_content_type() == "text/html":
59
- if html is None:
60
- html = bytes()
61
- html += part.get_payload(decode=True)
72
+ html.extend(part.get_payload(decode=True) or b"")
62
73
 
63
74
  return {
64
- 'body' : body,
65
- 'html' : html,
66
- 'attachments': attachments,
75
+ "body": body.decode("utf-8") if body else None,
76
+ "html": html.decode("utf-8") if html else None,
77
+ "attachments": attachments,
67
78
  }