velocity-python 0.0.105__py3-none-any.whl ā 0.0.155__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.
- velocity/__init__.py +3 -1
- velocity/app/orders.py +3 -4
- velocity/app/tests/__init__.py +1 -0
- velocity/app/tests/test_email_processing.py +112 -0
- velocity/app/tests/test_payment_profile_sorting.py +191 -0
- velocity/app/tests/test_spreadsheet_functions.py +124 -0
- velocity/aws/__init__.py +3 -0
- velocity/aws/amplify.py +10 -6
- velocity/aws/handlers/__init__.py +2 -0
- velocity/aws/handlers/base_handler.py +248 -0
- velocity/aws/handlers/context.py +167 -2
- velocity/aws/handlers/exceptions.py +16 -0
- velocity/aws/handlers/lambda_handler.py +24 -85
- velocity/aws/handlers/mixins/__init__.py +16 -0
- velocity/aws/handlers/mixins/activity_tracker.py +181 -0
- velocity/aws/handlers/mixins/aws_session_mixin.py +192 -0
- velocity/aws/handlers/mixins/error_handler.py +192 -0
- velocity/aws/handlers/mixins/legacy_mixin.py +53 -0
- velocity/aws/handlers/mixins/standard_mixin.py +73 -0
- velocity/aws/handlers/response.py +1 -1
- velocity/aws/handlers/sqs_handler.py +28 -143
- velocity/aws/tests/__init__.py +1 -0
- velocity/aws/tests/test_lambda_handler_json_serialization.py +120 -0
- velocity/aws/tests/test_response.py +163 -0
- velocity/db/__init__.py +16 -4
- velocity/db/core/decorators.py +20 -4
- velocity/db/core/engine.py +185 -792
- velocity/db/core/result.py +36 -22
- velocity/db/core/row.py +15 -3
- velocity/db/core/table.py +283 -44
- velocity/db/core/transaction.py +19 -11
- velocity/db/exceptions.py +42 -18
- velocity/db/servers/base/__init__.py +9 -0
- velocity/db/servers/base/initializer.py +70 -0
- velocity/db/servers/base/operators.py +98 -0
- velocity/db/servers/base/sql.py +503 -0
- velocity/db/servers/base/types.py +135 -0
- velocity/db/servers/mysql/__init__.py +73 -0
- velocity/db/servers/mysql/operators.py +54 -0
- velocity/db/servers/{mysql_reserved.py ā mysql/reserved.py} +2 -14
- velocity/db/servers/mysql/sql.py +718 -0
- velocity/db/servers/mysql/types.py +107 -0
- velocity/db/servers/postgres/__init__.py +59 -11
- velocity/db/servers/postgres/operators.py +34 -0
- velocity/db/servers/postgres/sql.py +474 -120
- velocity/db/servers/postgres/types.py +88 -2
- velocity/db/servers/sqlite/__init__.py +61 -0
- velocity/db/servers/sqlite/operators.py +52 -0
- velocity/db/servers/sqlite/reserved.py +20 -0
- velocity/db/servers/sqlite/sql.py +677 -0
- velocity/db/servers/sqlite/types.py +92 -0
- velocity/db/servers/sqlserver/__init__.py +73 -0
- velocity/db/servers/sqlserver/operators.py +47 -0
- velocity/db/servers/sqlserver/reserved.py +32 -0
- velocity/db/servers/sqlserver/sql.py +805 -0
- velocity/db/servers/sqlserver/types.py +114 -0
- velocity/db/servers/tablehelper.py +117 -91
- velocity/db/tests/__init__.py +1 -0
- velocity/db/tests/common_db_test.py +0 -0
- velocity/db/tests/postgres/__init__.py +1 -0
- velocity/db/tests/postgres/common.py +49 -0
- velocity/db/tests/postgres/test_column.py +29 -0
- velocity/db/tests/postgres/test_connections.py +25 -0
- velocity/db/tests/postgres/test_database.py +21 -0
- velocity/db/tests/postgres/test_engine.py +205 -0
- velocity/db/tests/postgres/test_general_usage.py +88 -0
- velocity/db/tests/postgres/test_imports.py +8 -0
- velocity/db/tests/postgres/test_result.py +19 -0
- velocity/db/tests/postgres/test_row.py +137 -0
- velocity/db/tests/postgres/test_row_comprehensive.py +720 -0
- velocity/db/tests/postgres/test_schema_locking.py +335 -0
- velocity/db/tests/postgres/test_schema_locking_unit.py +115 -0
- velocity/db/tests/postgres/test_sequence.py +34 -0
- velocity/db/tests/postgres/test_sql_comprehensive.py +462 -0
- velocity/db/tests/postgres/test_table.py +101 -0
- velocity/db/tests/postgres/test_table_comprehensive.py +646 -0
- velocity/db/tests/postgres/test_transaction.py +106 -0
- velocity/db/tests/sql/__init__.py +1 -0
- velocity/db/tests/sql/common.py +177 -0
- velocity/db/tests/sql/test_postgres_select_advanced.py +285 -0
- velocity/db/tests/sql/test_postgres_select_variances.py +517 -0
- velocity/db/tests/test_cursor_rowcount_fix.py +150 -0
- velocity/db/tests/test_db_utils.py +221 -0
- velocity/db/tests/test_postgres.py +448 -0
- velocity/db/tests/test_postgres_unchanged.py +81 -0
- velocity/db/tests/test_process_error_robustness.py +292 -0
- velocity/db/tests/test_result_caching.py +279 -0
- velocity/db/tests/test_result_sql_aware.py +117 -0
- velocity/db/tests/test_row_get_missing_column.py +72 -0
- velocity/db/tests/test_schema_locking_initializers.py +226 -0
- velocity/db/tests/test_schema_locking_simple.py +97 -0
- velocity/db/tests/test_sql_builder.py +165 -0
- velocity/db/tests/test_tablehelper.py +486 -0
- velocity/db/utils.py +62 -47
- velocity/misc/conv/__init__.py +2 -0
- velocity/misc/conv/iconv.py +5 -4
- velocity/misc/export.py +1 -4
- velocity/misc/merge.py +1 -1
- velocity/misc/tests/__init__.py +1 -0
- velocity/misc/tests/test_db.py +90 -0
- velocity/misc/tests/test_fix.py +78 -0
- velocity/misc/tests/test_format.py +64 -0
- velocity/misc/tests/test_iconv.py +203 -0
- velocity/misc/tests/test_merge.py +82 -0
- velocity/misc/tests/test_oconv.py +144 -0
- velocity/misc/tests/test_original_error.py +52 -0
- velocity/misc/tests/test_timer.py +74 -0
- velocity/misc/tools.py +0 -1
- {velocity_python-0.0.105.dist-info ā velocity_python-0.0.155.dist-info}/METADATA +2 -2
- velocity_python-0.0.155.dist-info/RECORD +129 -0
- velocity/db/core/exceptions.py +0 -70
- velocity/db/servers/mysql.py +0 -641
- velocity/db/servers/sqlite.py +0 -968
- velocity/db/servers/sqlite_reserved.py +0 -208
- velocity/db/servers/sqlserver.py +0 -921
- velocity/db/servers/sqlserver_reserved.py +0 -314
- velocity_python-0.0.105.dist-info/RECORD +0 -56
- {velocity_python-0.0.105.dist-info ā velocity_python-0.0.155.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.105.dist-info ā velocity_python-0.0.155.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.105.dist-info ā velocity_python-0.0.155.dist-info}/top_level.txt +0 -0
velocity/__init__.py
CHANGED
velocity/app/orders.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import support.app
|
|
3
3
|
import pprint
|
|
4
|
+
import velocity.db
|
|
4
5
|
|
|
5
|
-
engine =
|
|
6
|
+
engine = velocity.db.postgres.initialize()
|
|
6
7
|
REQUIRED = object()
|
|
7
8
|
|
|
8
9
|
|
|
@@ -114,9 +115,7 @@ class Order:
|
|
|
114
115
|
for key, default in defaults.items():
|
|
115
116
|
if key not in target:
|
|
116
117
|
target[key] = default() if callable(default) else default
|
|
117
|
-
|
|
118
|
-
# Always update updated_at if present
|
|
119
|
-
target[key] = default() if callable(default) else default
|
|
118
|
+
|
|
120
119
|
|
|
121
120
|
def _validate(self):
|
|
122
121
|
self._apply_defaults()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# App module tests
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from email.mime.text import MIMEText
|
|
3
|
+
from email.mime.multipart import MIMEMultipart
|
|
4
|
+
from email.mime.application import MIMEApplication
|
|
5
|
+
from velocity.misc.mail import (
|
|
6
|
+
Attachment,
|
|
7
|
+
get_full_emails,
|
|
8
|
+
get_address_only,
|
|
9
|
+
parse_attachment,
|
|
10
|
+
parse,
|
|
11
|
+
)
|
|
12
|
+
import hashlib
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestEmailProcessing(unittest.TestCase):
|
|
16
|
+
|
|
17
|
+
def test_get_full_emails(self):
|
|
18
|
+
"""Test that email addresses with names are formatted correctly."""
|
|
19
|
+
mock_address = [
|
|
20
|
+
type(
|
|
21
|
+
"Address",
|
|
22
|
+
(),
|
|
23
|
+
{"mailbox": b"john", "host": b"example.com", "name": b"John Doe"},
|
|
24
|
+
),
|
|
25
|
+
type(
|
|
26
|
+
"Address",
|
|
27
|
+
(),
|
|
28
|
+
{"mailbox": b"jane", "host": b"example.com", "name": None},
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
result = get_full_emails(mock_address)
|
|
32
|
+
self.assertEqual(result, ["John Doe <john@example.com>", "jane@example.com"])
|
|
33
|
+
|
|
34
|
+
def test_get_address_only(self):
|
|
35
|
+
"""Test that only email addresses without names are returned."""
|
|
36
|
+
mock_address = [
|
|
37
|
+
type("Address", (), {"mailbox": b"john", "host": b"example.com"}),
|
|
38
|
+
type("Address", (), {"mailbox": b"jane", "host": b"example.com"}),
|
|
39
|
+
]
|
|
40
|
+
result = get_address_only(mock_address)
|
|
41
|
+
self.assertEqual(result, ["john@example.com", "jane@example.com"])
|
|
42
|
+
|
|
43
|
+
def test_attachment_initialization(self):
|
|
44
|
+
"""Test the initialization of the Attachment class."""
|
|
45
|
+
data = b"file data"
|
|
46
|
+
attachment = Attachment(name="file.txt", data=data)
|
|
47
|
+
self.assertEqual(attachment.name, "file.txt")
|
|
48
|
+
self.assertEqual(attachment.data, data)
|
|
49
|
+
self.assertEqual(attachment.size, len(data))
|
|
50
|
+
self.assertEqual(attachment.ctype, "text/plain")
|
|
51
|
+
self.assertEqual(attachment.hash, hashlib.sha1(data).hexdigest())
|
|
52
|
+
|
|
53
|
+
def test_parse_attachment(self):
|
|
54
|
+
"""Test parsing a valid attachment from a message part."""
|
|
55
|
+
part = MIMEApplication(b"Test file data", Name="test.txt")
|
|
56
|
+
part["Content-Disposition"] = 'attachment; filename="test.txt"'
|
|
57
|
+
attachment = parse_attachment(part)
|
|
58
|
+
self.assertIsInstance(attachment, Attachment)
|
|
59
|
+
self.assertEqual(attachment.name, "test.txt")
|
|
60
|
+
self.assertEqual(attachment.ctype, "text/plain")
|
|
61
|
+
self.assertEqual(attachment.data, b"Test file data")
|
|
62
|
+
|
|
63
|
+
def test_parse_attachment_none(self):
|
|
64
|
+
"""Test that parse_attachment returns None if there's no attachment."""
|
|
65
|
+
part = MIMEText("This is a plain text email part")
|
|
66
|
+
self.assertIsNone(parse_attachment(part))
|
|
67
|
+
|
|
68
|
+
def test_parse_plain_text_email(self):
|
|
69
|
+
"""Test parsing a plain text email."""
|
|
70
|
+
msg = MIMEText("This is a plain text email")
|
|
71
|
+
msg["Content-Type"] = "text/plain"
|
|
72
|
+
result = parse(msg.as_string())
|
|
73
|
+
self.assertEqual(result["body"], "This is a plain text email")
|
|
74
|
+
self.assertIsNone(result["html"])
|
|
75
|
+
self.assertEqual(result["attachments"], [])
|
|
76
|
+
|
|
77
|
+
def test_parse_html_email(self):
|
|
78
|
+
"""Test parsing an HTML email."""
|
|
79
|
+
msg = MIMEText("<p>This is an HTML email</p>", "html")
|
|
80
|
+
msg["Content-Type"] = "text/html"
|
|
81
|
+
result = parse(msg.as_string())
|
|
82
|
+
self.assertEqual(result["html"], "<p>This is an HTML email</p>")
|
|
83
|
+
self.assertIsNone(result["body"])
|
|
84
|
+
self.assertEqual(result["attachments"], [])
|
|
85
|
+
|
|
86
|
+
def test_parse_multipart_email_with_attachments(self):
|
|
87
|
+
"""Test parsing a multipart email with attachments."""
|
|
88
|
+
msg = MIMEMultipart()
|
|
89
|
+
msg.attach(MIMEText("This is a plain text part", "plain"))
|
|
90
|
+
msg.attach(MIMEText("<p>This is an HTML part</p>", "html"))
|
|
91
|
+
|
|
92
|
+
attachment_part = MIMEApplication(b"Attachment data", Name="attachment.txt")
|
|
93
|
+
attachment_part["Content-Disposition"] = 'attachment; filename="attachment.txt"'
|
|
94
|
+
msg.attach(attachment_part)
|
|
95
|
+
|
|
96
|
+
result = parse(msg.as_string())
|
|
97
|
+
self.assertEqual(result["body"], "This is a plain text part")
|
|
98
|
+
self.assertEqual(result["html"], "<p>This is an HTML part</p>")
|
|
99
|
+
self.assertEqual(len(result["attachments"]), 1)
|
|
100
|
+
self.assertEqual(result["attachments"][0].name, "attachment.txt")
|
|
101
|
+
self.assertEqual(result["attachments"][0].data, b"Attachment data")
|
|
102
|
+
|
|
103
|
+
def test_parse_empty_email(self):
|
|
104
|
+
"""Test parsing an empty email string."""
|
|
105
|
+
result = parse("")
|
|
106
|
+
self.assertIsNone(result["body"])
|
|
107
|
+
self.assertIsNone(result["html"])
|
|
108
|
+
self.assertEqual(result["attachments"], [])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
unittest.main()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test for payment profile sorting with None expiration dates.
|
|
4
|
+
|
|
5
|
+
This addresses the production error:
|
|
6
|
+
TypeError: '<' not supported between instances of 'NoneType' and 'NoneType'
|
|
7
|
+
|
|
8
|
+
The error occurs when sorting payment profiles by expiration_date when some values are None.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import unittest
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestPaymentProfileSorting(unittest.TestCase):
|
|
16
|
+
"""Test sorting payment profiles with various expiration date scenarios."""
|
|
17
|
+
|
|
18
|
+
def setUp(self):
|
|
19
|
+
"""Set up test data with various expiration date scenarios."""
|
|
20
|
+
self.payment_profiles = [
|
|
21
|
+
{
|
|
22
|
+
"sys_id": 1,
|
|
23
|
+
"email_address": "test1@example.com",
|
|
24
|
+
"card_number": "1234",
|
|
25
|
+
"expiration_date": "2024-12",
|
|
26
|
+
"status": "active",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"sys_id": 2,
|
|
30
|
+
"email_address": "test1@example.com",
|
|
31
|
+
"card_number": "1234",
|
|
32
|
+
"expiration_date": "2024-06",
|
|
33
|
+
"status": "active",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"sys_id": 3,
|
|
37
|
+
"email_address": "test1@example.com",
|
|
38
|
+
"card_number": "1234",
|
|
39
|
+
"expiration_date": None,
|
|
40
|
+
"status": "active",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"sys_id": 4,
|
|
44
|
+
"email_address": "test1@example.com",
|
|
45
|
+
"card_number": "1234",
|
|
46
|
+
"expiration_date": "2025-01",
|
|
47
|
+
"status": "active",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"sys_id": 5,
|
|
51
|
+
"email_address": "test1@example.com",
|
|
52
|
+
"card_number": "1234",
|
|
53
|
+
"expiration_date": None,
|
|
54
|
+
"status": "active",
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
def test_original_error_reproduction(self):
|
|
59
|
+
"""Reproduce the original error to confirm it happens."""
|
|
60
|
+
with self.assertRaises(TypeError) as context:
|
|
61
|
+
# This should fail with the original error
|
|
62
|
+
sorted(self.payment_profiles, key=lambda x: x["expiration_date"])
|
|
63
|
+
|
|
64
|
+
self.assertIn(
|
|
65
|
+
"'<' not supported between instances of 'NoneType'", str(context.exception)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def test_safe_sorting_with_none_last(self):
|
|
69
|
+
"""Test sorting with None values placed at the end."""
|
|
70
|
+
|
|
71
|
+
def safe_sort_key_none_last(row):
|
|
72
|
+
"""Sort key that places None values at the end."""
|
|
73
|
+
exp_date = row["expiration_date"]
|
|
74
|
+
if exp_date is None:
|
|
75
|
+
return (1, "") # (1, "") sorts after (0, any_date)
|
|
76
|
+
return (0, exp_date)
|
|
77
|
+
|
|
78
|
+
sorted_profiles = sorted(self.payment_profiles, key=safe_sort_key_none_last)
|
|
79
|
+
|
|
80
|
+
# Check that non-None dates come first and are in order
|
|
81
|
+
non_none_dates = [
|
|
82
|
+
p["expiration_date"]
|
|
83
|
+
for p in sorted_profiles
|
|
84
|
+
if p["expiration_date"] is not None
|
|
85
|
+
]
|
|
86
|
+
self.assertEqual(non_none_dates, ["2024-06", "2024-12", "2025-01"])
|
|
87
|
+
|
|
88
|
+
# Check that None values come last
|
|
89
|
+
none_dates = [
|
|
90
|
+
p["expiration_date"]
|
|
91
|
+
for p in sorted_profiles
|
|
92
|
+
if p["expiration_date"] is None
|
|
93
|
+
]
|
|
94
|
+
self.assertEqual(len(none_dates), 2)
|
|
95
|
+
|
|
96
|
+
# Verify the full sort order
|
|
97
|
+
expected_order = ["2024-06", "2024-12", "2025-01", None, None]
|
|
98
|
+
actual_order = [p["expiration_date"] for p in sorted_profiles]
|
|
99
|
+
self.assertEqual(actual_order, expected_order)
|
|
100
|
+
|
|
101
|
+
def test_safe_sorting_with_none_first(self):
|
|
102
|
+
"""Test sorting with None values placed at the beginning."""
|
|
103
|
+
|
|
104
|
+
def safe_sort_key_none_first(row):
|
|
105
|
+
"""Sort key that places None values at the beginning."""
|
|
106
|
+
exp_date = row["expiration_date"]
|
|
107
|
+
if exp_date is None:
|
|
108
|
+
return (0, "") # (0, "") sorts before (1, any_date)
|
|
109
|
+
return (1, exp_date)
|
|
110
|
+
|
|
111
|
+
sorted_profiles = sorted(self.payment_profiles, key=safe_sort_key_none_first)
|
|
112
|
+
|
|
113
|
+
# Check that None values come first
|
|
114
|
+
none_count = sum(1 for p in sorted_profiles[:2] if p["expiration_date"] is None)
|
|
115
|
+
self.assertEqual(none_count, 2)
|
|
116
|
+
|
|
117
|
+
# Check that non-None dates come after and are in order
|
|
118
|
+
non_none_dates = [
|
|
119
|
+
p["expiration_date"]
|
|
120
|
+
for p in sorted_profiles
|
|
121
|
+
if p["expiration_date"] is not None
|
|
122
|
+
]
|
|
123
|
+
self.assertEqual(non_none_dates, ["2024-06", "2024-12", "2025-01"])
|
|
124
|
+
|
|
125
|
+
def test_safe_sorting_with_default_date(self):
|
|
126
|
+
"""Test sorting with None values replaced by a default date."""
|
|
127
|
+
|
|
128
|
+
def safe_sort_key_with_default(row):
|
|
129
|
+
"""Sort key that replaces None with a default date."""
|
|
130
|
+
exp_date = row["expiration_date"]
|
|
131
|
+
if exp_date is None:
|
|
132
|
+
return "1900-01" # Very old date so None values sort first
|
|
133
|
+
return exp_date
|
|
134
|
+
|
|
135
|
+
sorted_profiles = sorted(self.payment_profiles, key=safe_sort_key_with_default)
|
|
136
|
+
|
|
137
|
+
# Check that None values (now "1900-01") come first
|
|
138
|
+
none_count = sum(1 for p in sorted_profiles[:2] if p["expiration_date"] is None)
|
|
139
|
+
self.assertEqual(none_count, 2)
|
|
140
|
+
|
|
141
|
+
# Check the overall order
|
|
142
|
+
actual_order = [p["expiration_date"] for p in sorted_profiles]
|
|
143
|
+
expected_order = [None, None, "2024-06", "2024-12", "2025-01"]
|
|
144
|
+
self.assertEqual(actual_order, expected_order)
|
|
145
|
+
|
|
146
|
+
def test_grouped_sorting_scenario(self):
|
|
147
|
+
"""Test the specific scenario from the billing handler."""
|
|
148
|
+
# Group profiles by email and card number (as in the original code)
|
|
149
|
+
groups = {}
|
|
150
|
+
for row in self.payment_profiles:
|
|
151
|
+
key = (row["email_address"], row["card_number"])
|
|
152
|
+
if key not in groups:
|
|
153
|
+
groups[key] = []
|
|
154
|
+
groups[key].append(row)
|
|
155
|
+
|
|
156
|
+
# Process each group with safe sorting
|
|
157
|
+
for group in groups.values():
|
|
158
|
+
# This should not raise an error
|
|
159
|
+
def safe_sort_key(row):
|
|
160
|
+
exp_date = row["expiration_date"]
|
|
161
|
+
return (exp_date is None, exp_date or "")
|
|
162
|
+
|
|
163
|
+
sorted_group = sorted(group, key=safe_sort_key)
|
|
164
|
+
|
|
165
|
+
# Verify we can enumerate through the sorted group
|
|
166
|
+
for idx, row in enumerate(sorted_group):
|
|
167
|
+
self.assertIsInstance(idx, int)
|
|
168
|
+
self.assertIn("sys_id", row)
|
|
169
|
+
self.assertIn("expiration_date", row)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def safe_expiration_date_sort_key(row):
|
|
173
|
+
"""
|
|
174
|
+
Safe sorting key for payment profiles by expiration date.
|
|
175
|
+
|
|
176
|
+
Handles None values by placing them at the end of the sort order.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
row: Dictionary representing a payment profile with 'expiration_date' key
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Tuple that can be safely sorted, with None values last
|
|
183
|
+
"""
|
|
184
|
+
exp_date = row["expiration_date"]
|
|
185
|
+
if exp_date is None:
|
|
186
|
+
return (1, "") # (1, "") sorts after (0, any_date)
|
|
187
|
+
return (0, exp_date)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
unittest.main()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
import base64
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from openpyxl import load_workbook
|
|
5
|
+
from velocity.misc.export import (
|
|
6
|
+
extract,
|
|
7
|
+
autosize_columns,
|
|
8
|
+
create_spreadsheet,
|
|
9
|
+
get_downloadable_spreadsheet,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestSpreadsheetFunctions(unittest.TestCase):
|
|
14
|
+
|
|
15
|
+
def test_extract(self):
|
|
16
|
+
"""Test extracting values from a dictionary based on a list of keys."""
|
|
17
|
+
data = {"name": "Alice", "age": 30, "city": "Wonderland"}
|
|
18
|
+
keys = ["name", "city", "nonexistent_key"]
|
|
19
|
+
result = extract(data, keys)
|
|
20
|
+
self.assertEqual(result, ["Alice", "Wonderland", None])
|
|
21
|
+
|
|
22
|
+
def test_autosize_columns(self):
|
|
23
|
+
"""Test that the columns are autosized based on the content."""
|
|
24
|
+
buffer = BytesIO()
|
|
25
|
+
headers = ["Column1", "Column2"]
|
|
26
|
+
rows = [["Short", "This is a longer text that should set the column width"]]
|
|
27
|
+
|
|
28
|
+
# Create a workbook and worksheet
|
|
29
|
+
create_spreadsheet(headers, rows, buffer)
|
|
30
|
+
buffer.seek(0)
|
|
31
|
+
workbook = load_workbook(buffer)
|
|
32
|
+
worksheet = workbook.active
|
|
33
|
+
|
|
34
|
+
# Run autosize_columns on the loaded worksheet
|
|
35
|
+
autosize_columns(worksheet)
|
|
36
|
+
|
|
37
|
+
# Check if the column widths were adjusted properly
|
|
38
|
+
col1_width = worksheet.column_dimensions["A"].width
|
|
39
|
+
col2_width = worksheet.column_dimensions["B"].width
|
|
40
|
+
self.assertGreater(col2_width, col1_width)
|
|
41
|
+
|
|
42
|
+
def test_create_spreadsheet_basic(self):
|
|
43
|
+
"""Test creating a spreadsheet with headers and rows."""
|
|
44
|
+
buffer = BytesIO()
|
|
45
|
+
headers = ["Header1", "Header2"]
|
|
46
|
+
rows = [["Row1-Col1", "Row1-Col2"], ["Row2-Col1", "Row2-Col2"]]
|
|
47
|
+
|
|
48
|
+
create_spreadsheet(headers, rows, buffer)
|
|
49
|
+
buffer.seek(0)
|
|
50
|
+
workbook = load_workbook(buffer)
|
|
51
|
+
worksheet = workbook.active
|
|
52
|
+
|
|
53
|
+
# Verify headers and rows
|
|
54
|
+
self.assertEqual(worksheet["A1"].value, "Header1")
|
|
55
|
+
self.assertEqual(worksheet["B1"].value, "Header2")
|
|
56
|
+
self.assertEqual(worksheet["A2"].value, "Row1-Col1")
|
|
57
|
+
self.assertEqual(worksheet["B2"].value, "Row1-Col2")
|
|
58
|
+
self.assertEqual(worksheet["A3"].value, "Row2-Col1")
|
|
59
|
+
self.assertEqual(worksheet["B3"].value, "Row2-Col2")
|
|
60
|
+
|
|
61
|
+
def test_create_spreadsheet_with_styles_and_merge(self):
|
|
62
|
+
"""Test creating a spreadsheet with custom styles and merged cells."""
|
|
63
|
+
buffer = BytesIO()
|
|
64
|
+
headers = ["Header1", "Header2"]
|
|
65
|
+
rows = [["Row1-Col1", "Row1-Col2"]]
|
|
66
|
+
styles = {"A1": "col_header", "B1": "col_header"}
|
|
67
|
+
merge = ["A1:B1"]
|
|
68
|
+
|
|
69
|
+
create_spreadsheet(headers, rows, buffer, styles=styles, merge=merge)
|
|
70
|
+
buffer.seek(0)
|
|
71
|
+
workbook = load_workbook(buffer)
|
|
72
|
+
worksheet = workbook.active
|
|
73
|
+
|
|
74
|
+
# Verify merged cells and styles
|
|
75
|
+
self.assertTrue(worksheet.merged_cells.ranges)
|
|
76
|
+
self.assertEqual(str(worksheet.merged_cells.ranges[0]), "A1:B1")
|
|
77
|
+
self.assertEqual(worksheet["A1"].style, "col_header")
|
|
78
|
+
self.assertEqual(worksheet["B1"].style, "col_header")
|
|
79
|
+
|
|
80
|
+
def test_create_spreadsheet_with_freeze_panes_and_dimensions(self):
|
|
81
|
+
"""Test creating a spreadsheet with freeze panes and custom row/column dimensions."""
|
|
82
|
+
buffer = BytesIO()
|
|
83
|
+
headers = ["Header1", "Header2"]
|
|
84
|
+
rows = [["Row1-Col1", "Row1-Col2"]]
|
|
85
|
+
dimensions = {"rows": {1: 25}, "columns": {"A": 20, "B": 30}}
|
|
86
|
+
|
|
87
|
+
create_spreadsheet(
|
|
88
|
+
headers, rows, buffer, freeze_panes="A2", dimensions=dimensions
|
|
89
|
+
)
|
|
90
|
+
buffer.seek(0)
|
|
91
|
+
workbook = load_workbook(buffer)
|
|
92
|
+
worksheet = workbook.active
|
|
93
|
+
|
|
94
|
+
# Verify freeze panes and custom dimensions
|
|
95
|
+
self.assertEqual(worksheet.freeze_panes, "A2")
|
|
96
|
+
self.assertEqual(worksheet.row_dimensions[1].height, 25)
|
|
97
|
+
self.assertEqual(worksheet.column_dimensions["A"].width, 20)
|
|
98
|
+
self.assertEqual(worksheet.column_dimensions["B"].width, 30)
|
|
99
|
+
|
|
100
|
+
def test_get_downloadable_spreadsheet(self):
|
|
101
|
+
"""Test generating a downloadable spreadsheet encoded in base64."""
|
|
102
|
+
headers = ["Header1", "Header2"]
|
|
103
|
+
rows = [["Row1-Col1", "Row1-Col2"], ["Row2-Col1", "Row2-Col2"]]
|
|
104
|
+
|
|
105
|
+
# Generate the base64-encoded spreadsheet
|
|
106
|
+
encoded_spreadsheet = get_downloadable_spreadsheet(headers, rows)
|
|
107
|
+
decoded_data = base64.b64decode(encoded_spreadsheet)
|
|
108
|
+
|
|
109
|
+
# Load the spreadsheet from the decoded data and verify its content
|
|
110
|
+
buffer = BytesIO(decoded_data)
|
|
111
|
+
workbook = load_workbook(buffer)
|
|
112
|
+
worksheet = workbook.active
|
|
113
|
+
|
|
114
|
+
# Verify headers and rows
|
|
115
|
+
self.assertEqual(worksheet["A1"].value, "Header1")
|
|
116
|
+
self.assertEqual(worksheet["B1"].value, "Header2")
|
|
117
|
+
self.assertEqual(worksheet["A2"].value, "Row1-Col1")
|
|
118
|
+
self.assertEqual(worksheet["B2"].value, "Row1-Col2")
|
|
119
|
+
self.assertEqual(worksheet["A3"].value, "Row2-Col1")
|
|
120
|
+
self.assertEqual(worksheet["B3"].value, "Row2-Col2")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
unittest.main()
|
velocity/aws/__init__.py
CHANGED
velocity/aws/amplify.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import boto3
|
|
2
|
+
import time
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class AmplifyProject:
|
|
@@ -325,7 +326,12 @@ class AmplifyProject:
|
|
|
325
326
|
"ec2:AttachNetworkInterface",
|
|
326
327
|
"ec2:DescribeNetworkInterfaces",
|
|
327
328
|
"ec2:DeleteNetworkInterface",
|
|
328
|
-
"
|
|
329
|
+
"ec2:DescribeSecurityGroups",
|
|
330
|
+
"ec2:AuthorizeSecurityGroupIngress",
|
|
331
|
+
"ec2:AuthorizeSecurityGroupEgress",
|
|
332
|
+
"ec2:RevokeSecurityGroupIngress",
|
|
333
|
+
"ec2:RevokeSecurityGroupEgress",
|
|
334
|
+
"cognito-idp:*"
|
|
329
335
|
],
|
|
330
336
|
}
|
|
331
337
|
],
|
|
@@ -389,10 +395,9 @@ class AmplifyProject:
|
|
|
389
395
|
except Exception:
|
|
390
396
|
pass
|
|
391
397
|
|
|
392
|
-
def check_policies(
|
|
393
|
-
self,
|
|
394
|
-
):
|
|
398
|
+
def check_policies(self, function):
|
|
395
399
|
# Attach a role policy
|
|
400
|
+
iam_client = boto3.client("iam")
|
|
396
401
|
response = iam_client.list_attached_role_policies(
|
|
397
402
|
RoleName=function["Role"].split("/")[1]
|
|
398
403
|
)
|
|
@@ -426,7 +431,7 @@ class AmplifyProject:
|
|
|
426
431
|
print(f"\nš§ Applying {len(env_vars)} environment variables...")
|
|
427
432
|
for function_name in self.list_lambda_functions_filtered(branch):
|
|
428
433
|
print(f"ā”ļø Updating Lambda function: {function_name}")
|
|
429
|
-
self.
|
|
434
|
+
self.update_lambda_function(function_name, env_vars)
|
|
430
435
|
|
|
431
436
|
print(
|
|
432
437
|
"ā
Environment variables successfully applied to matching Lambda functions.\n"
|
|
@@ -435,7 +440,6 @@ class AmplifyProject:
|
|
|
435
440
|
|
|
436
441
|
def main():
|
|
437
442
|
app_id = "d3c209q3ri53mk"
|
|
438
|
-
branch = "demo"
|
|
439
443
|
app = AmplifyProject(app_id)
|
|
440
444
|
print(app.list_backend_branches())
|
|
441
445
|
|