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/__init__.py +1 -1
- velocity/aws/handlers/response.py +203 -52
- velocity/db/core/table.py +8 -2
- velocity/misc/conv/iconv.py +110 -143
- velocity/misc/conv/oconv.py +126 -134
- velocity/misc/export.py +102 -99
- velocity/misc/format.py +44 -47
- velocity/misc/mail.py +44 -40
- velocity/misc/merge.py +33 -17
- velocity/misc/timer.py +33 -10
- {velocity_python-0.0.31.dist-info → velocity_python-0.0.33.dist-info}/METADATA +1 -1
- {velocity_python-0.0.31.dist-info → velocity_python-0.0.33.dist-info}/RECORD +15 -15
- {velocity_python-0.0.31.dist-info → velocity_python-0.0.33.dist-info}/LICENSE +0 -0
- {velocity_python-0.0.31.dist-info → velocity_python-0.0.33.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.31.dist-info → velocity_python-0.0.33.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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
|
-
|
|
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:
|
|
20
|
-
if len(str(cell.value)) > max_length:
|
|
21
|
-
max_length = len(cell.value)
|
|
22
|
-
except:
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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,
|
|
63
|
-
if hasattr(
|
|
64
|
-
return
|
|
65
|
-
elif
|
|
66
|
-
return
|
|
67
|
-
elif isinstance(
|
|
68
|
-
return float(
|
|
69
|
-
elif isinstance(
|
|
70
|
-
return
|
|
71
|
-
elif isinstance(
|
|
72
|
-
return
|
|
73
|
-
elif isinstance(
|
|
74
|
-
return
|
|
75
|
-
elif isinstance(
|
|
76
|
-
return human_delta(
|
|
77
|
-
|
|
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
|
|
4
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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"{
|
|
36
|
+
results.append(f"{mailbox}@{host}")
|
|
20
37
|
return results
|
|
21
38
|
|
|
22
39
|
|
|
23
|
-
def get_address_only(addresses):
|
|
24
|
-
|
|
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
|
|
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
|
|
40
|
-
return
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
+
Deeply merges multiple dictionaries.
|
|
9
|
+
|
|
8
10
|
Parameters
|
|
9
11
|
----------
|
|
10
|
-
dicts :
|
|
11
|
-
|
|
12
|
-
update : bool
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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(
|
|
24
|
-
elif isinstance(d1[key], dict):
|
|
25
|
-
d1[key] = merge_into(d1[key],
|
|
26
|
-
elif isinstance(d1[key], list) and isinstance(
|
|
27
|
-
d1[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(
|
|
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
|
-
|
|
11
|
-
self.
|
|
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
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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.
|
|
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
|