velocity-python 0.0.31__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/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.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,9 +1,9 @@
1
- velocity/__init__.py,sha256=OeRnEfJy9NabRsxHYEQSJCo2jOlOxfmggEKOqDzaCXc,88
1
+ velocity/__init__.py,sha256=8Xcz_RUYvy6ibGGQeUrwa-fEVzDMhSKe2ZnYgytCeTw,88
2
2
  velocity/aws/__init__.py,sha256=GBTEr02whnCH3TG-BWCpUC3KfHY3uNxD21g0OvsVJnc,598
3
3
  velocity/aws/handlers/__init__.py,sha256=xnpFZJVlC2uoeeFW4zuPST8wA8ajaQDky5Y6iXZzi3A,172
4
4
  velocity/aws/handlers/context.py,sha256=UIjNR83y2NSIyK8HMPX8t5tpJHFNabiZvNgmmdQL3HA,1822
5
5
  velocity/aws/handlers/lambda_handler.py,sha256=RfEFIIn6a2k0W25AMEOMWCqbpUkXF13kV6vXFVKz0b0,6309
6
- velocity/aws/handlers/response.py,sha256=PlbrTFWZ6-2s7MGqaGAYENVuL63wOC7FlW7ZogFyTKo,3869
6
+ velocity/aws/handlers/response.py,sha256=LXhtizLKnVBWjtHyE0h0bk-NYDrRpj7CHa7tRz9KkC4,9324
7
7
  velocity/aws/handlers/sqs_handler.py,sha256=YBqrEkA6EfkQUVk_kwsSI-HjFJO8-JqYco-p0UYDNXE,3368
8
8
  velocity/db/__init__.py,sha256=vrn2AFNAKaqTdnPwLFS0OcREcCtzUCOodlmH54U7ADg,200
9
9
  velocity/db/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -24,16 +24,16 @@ velocity/db/servers/sqlite.py,sha256=_PGg6ECHdOyOMn4tTWX4WlectAukW3Dov7cZxHbVtBM
24
24
  velocity/db/servers/sqlserver.py,sha256=ACdkTyJzdFzN-RpmOabKgmMPtCdHzT-FwYH00UpSNyM,34465
25
25
  velocity/misc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  velocity/misc/db.py,sha256=MtuNa7Z6F4SBy9wCXqLpSzWOJMkx51Z3vpUxn4FgUqU,2850
27
- velocity/misc/export.py,sha256=LgvlexQal3O2OMvMuvWmY17PzK2xsv1cG44YXnBDSig,4348
28
- velocity/misc/format.py,sha256=5hUVxBR1r-e0jrQwxiZx5dU5u-TsMnRVuCFNuczTpAs,2302
29
- velocity/misc/mail.py,sha256=SmX-9v79lxOflgcc-L-ME9HW07sfayIiuCg1zA3EDIw,2129
30
- velocity/misc/merge.py,sha256=ouzXOab7BX5Codo_goHqvrFMfjlRn8nVnUZj9A8MRMk,902
31
- velocity/misc/timer.py,sha256=HEV64eQPTNx_3aeUTCP2_st4zPDHEDmclQM9R5r7wDg,537
27
+ velocity/misc/export.py,sha256=lATvwTe-Of6X7ZtzvJZiFghh9QlyMYZfDfQ_GJyt5yg,5197
28
+ velocity/misc/format.py,sha256=OnjkxEEKzKuwihdf570VDSpsqOJLp0iyW0cuyDaxL-U,2649
29
+ velocity/misc/mail.py,sha256=BrxDqeVsOd0epyJKwrHA-owzs6di2oLA_qJskoTux-c,2553
30
+ velocity/misc/merge.py,sha256=EYtqwnckBllPO60tRALxFRuzmUQ7Wl0qZC6sCgyiZDA,1885
31
+ velocity/misc/timer.py,sha256=cN3aS0t6HLlhYfF2Ir6ihJehxNrWf9ebaLzXUaWRKEA,1637
32
32
  velocity/misc/conv/__init__.py,sha256=MLYF58QHjzfDSxb1rdnmLnuEQCa3gnhzzZ30CwZVvQo,40
33
- velocity/misc/conv/iconv.py,sha256=V-PcpDXq7wCmyxZ3sXfzpmVH5Nc9OHScvMH26K7N3bI,4375
34
- velocity/misc/conv/oconv.py,sha256=p5_lNU7SHIp6vSBzDaAZJ7M2w1Hs7zSn5z6Hndvfb0o,4071
35
- velocity_python-0.0.31.dist-info/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
36
- velocity_python-0.0.31.dist-info/METADATA,sha256=EB7kL6jeO1lXfC97K8WPELfDJIp49N_LqgieOxKg0BA,8522
37
- velocity_python-0.0.31.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
38
- velocity_python-0.0.31.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
39
- velocity_python-0.0.31.dist-info/RECORD,,
33
+ velocity/misc/conv/iconv.py,sha256=ThTT3_t0Us5P7UuBa58ko-GKVm30FFUrSmPBd6Gfh90,5660
34
+ velocity/misc/conv/oconv.py,sha256=XdRTQnywWNwVc0FH-8hPHjlTpZNFg80ivIfyTV3mMak,5495
35
+ velocity_python-0.0.32.dist-info/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
36
+ velocity_python-0.0.32.dist-info/METADATA,sha256=teq0KrZ_b16Wx7QWDWpXIChNmAxJMiKbyjBMDt_9uf4,8522
37
+ velocity_python-0.0.32.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
38
+ velocity_python-0.0.32.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
39
+ velocity_python-0.0.32.dist-info/RECORD,,