memberjojo 1.0__tar.gz → 1.1__tar.gz
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.
- {memberjojo-1.0 → memberjojo-1.1}/PKG-INFO +3 -2
- {memberjojo-1.0 → memberjojo-1.1}/pyproject.toml +4 -2
- {memberjojo-1.0 → memberjojo-1.1}/src/memberjojo/_version.py +2 -2
- memberjojo-1.1/tests/test_mojo_members.py +185 -0
- memberjojo-1.1/tests/test_mojo_members_iter.py +66 -0
- memberjojo-1.1/tests/test_mojo_transactions.py +240 -0
- memberjojo-1.1/tests/test_version.py +27 -0
- {memberjojo-1.0 → memberjojo-1.1}/LICENSE +0 -0
- {memberjojo-1.0 → memberjojo-1.1}/README.md +0 -0
- {memberjojo-1.0 → memberjojo-1.1}/src/memberjojo/__init__.py +0 -0
- {memberjojo-1.0 → memberjojo-1.1}/src/memberjojo/config.py +0 -0
- {memberjojo-1.0 → memberjojo-1.1}/src/memberjojo/mojo_common.py +0 -0
- {memberjojo-1.0 → memberjojo-1.1}/src/memberjojo/mojo_member.py +0 -0
- {memberjojo-1.0 → memberjojo-1.1}/src/memberjojo/mojo_transaction.py +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memberjojo
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1
|
|
4
4
|
Summary: memberjojo - tools for working with members.
|
|
5
5
|
Author-email: Duncan Bellamy <dunk@denkimushi.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
6
7
|
Description-Content-Type: text/markdown
|
|
7
8
|
License-Expression: MIT
|
|
8
9
|
License-File: LICENSE
|
|
10
|
+
Requires-Dist: coverage ; extra == "dev"
|
|
9
11
|
Requires-Dist: flit ; extra == "dev"
|
|
10
12
|
Requires-Dist: pytest ; extra == "dev"
|
|
11
13
|
Requires-Dist: furo ; extra == "docs"
|
|
@@ -13,7 +15,6 @@ Requires-Dist: myst-parser>=2.0 ; extra == "docs"
|
|
|
13
15
|
Requires-Dist: sphinx>=7.0 ; extra == "docs"
|
|
14
16
|
Requires-Dist: sphinx-autodoc-typehints ; extra == "docs"
|
|
15
17
|
Requires-Dist: black ; extra == "lint"
|
|
16
|
-
Requires-Dist: coverage ; extra == "lint"
|
|
17
18
|
Requires-Dist: pylint ; extra == "lint"
|
|
18
19
|
Project-URL: Home, https://github.com/a16bitsysop/memberjojo
|
|
19
20
|
Provides-Extra: dev
|
|
@@ -9,18 +9,19 @@ readme = "README.md"
|
|
|
9
9
|
license = "MIT"
|
|
10
10
|
license-files = ["LICENSE"]
|
|
11
11
|
dynamic = ["version", "description"]
|
|
12
|
+
requires-python = ">=3.8"
|
|
12
13
|
|
|
13
14
|
[project.urls]
|
|
14
15
|
Home = "https://github.com/a16bitsysop/memberjojo"
|
|
15
16
|
|
|
16
17
|
[project.optional-dependencies]
|
|
17
18
|
dev = [
|
|
19
|
+
"coverage",
|
|
18
20
|
"flit",
|
|
19
21
|
"pytest",
|
|
20
22
|
]
|
|
21
23
|
lint = [
|
|
22
24
|
"black",
|
|
23
|
-
"coverage",
|
|
24
25
|
"pylint",
|
|
25
26
|
]
|
|
26
27
|
docs = [
|
|
@@ -31,7 +32,8 @@ docs = [
|
|
|
31
32
|
]
|
|
32
33
|
|
|
33
34
|
[tool.flit.sdist]
|
|
34
|
-
exclude = ["
|
|
35
|
+
exclude = ["**/.DS_Store"]
|
|
36
|
+
include = ["tests", "src/memberjojo/_version.py"]
|
|
35
37
|
|
|
36
38
|
[tool.setuptools_scm]
|
|
37
39
|
write_to = "src/memberjojo/_version.py"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the member module
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from csv import DictWriter
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import NamedTemporaryFile
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from memberjojo import Member
|
|
11
|
+
|
|
12
|
+
# pylint: disable=redefined-outer-name
|
|
13
|
+
# or pylint thinks fixtures are redined as function variables
|
|
14
|
+
# --- Fixtures & Helpers ---
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_csv_file(tmp_path):
|
|
19
|
+
"""
|
|
20
|
+
Create a temporary mock CSV file for testing.
|
|
21
|
+
Returns path to the CSV.
|
|
22
|
+
"""
|
|
23
|
+
fieldnames = [
|
|
24
|
+
"Member number",
|
|
25
|
+
"Title",
|
|
26
|
+
"First name",
|
|
27
|
+
"Last name",
|
|
28
|
+
"membermojo ID",
|
|
29
|
+
"Short URL",
|
|
30
|
+
]
|
|
31
|
+
rows = [
|
|
32
|
+
{
|
|
33
|
+
"Member number": "1",
|
|
34
|
+
"Title": "Mr",
|
|
35
|
+
"First name": "John",
|
|
36
|
+
"Last name": "Doe",
|
|
37
|
+
"membermojo ID": "1001",
|
|
38
|
+
"Short URL": "http://short.url/johndoe",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"Member number": "2",
|
|
42
|
+
"Title": "Ms",
|
|
43
|
+
"First name": "Jane",
|
|
44
|
+
"Last name": "Smith",
|
|
45
|
+
"membermojo ID": "1002",
|
|
46
|
+
"Short URL": "http://short.url/janesmith",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"Member number": "3",
|
|
50
|
+
"Title": "Dr",
|
|
51
|
+
"First name": "Emily",
|
|
52
|
+
"Last name": "Stone",
|
|
53
|
+
"membermojo ID": "1001",
|
|
54
|
+
"Short URL": "http://short.url/emilystone",
|
|
55
|
+
}, # duplicate ID
|
|
56
|
+
{
|
|
57
|
+
"Member number": "1",
|
|
58
|
+
"Title": "Mrs",
|
|
59
|
+
"First name": "Sara",
|
|
60
|
+
"Last name": "Connor",
|
|
61
|
+
"membermojo ID": "1003",
|
|
62
|
+
"Short URL": "http://short.url/saraconnor",
|
|
63
|
+
}, # duplicate number
|
|
64
|
+
{
|
|
65
|
+
"Member number": "4",
|
|
66
|
+
"Title": "Sir",
|
|
67
|
+
"First name": "Rick",
|
|
68
|
+
"Last name": "Grimes",
|
|
69
|
+
"membermojo ID": "1004",
|
|
70
|
+
"Short URL": "http://short.url/rickgrimes",
|
|
71
|
+
}, # invalid title
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
csv_path = tmp_path / "mock_data.csv"
|
|
75
|
+
with csv_path.open(mode="w", encoding="ISO-8859-1", newline="") as f:
|
|
76
|
+
writer = DictWriter(f, fieldnames=fieldnames)
|
|
77
|
+
writer.writeheader()
|
|
78
|
+
writer.writerows(rows)
|
|
79
|
+
|
|
80
|
+
return Path(csv_path)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.fixture
|
|
84
|
+
def db_path():
|
|
85
|
+
"""
|
|
86
|
+
Temp file for db connection
|
|
87
|
+
"""
|
|
88
|
+
with NamedTemporaryFile(suffix=".db", delete=False) as tmp:
|
|
89
|
+
path = Path(tmp.name)
|
|
90
|
+
yield path
|
|
91
|
+
path.unlink()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.fixture
|
|
95
|
+
def member_db(db_path, mock_csv_file):
|
|
96
|
+
"""
|
|
97
|
+
Test sqlite member database
|
|
98
|
+
"""
|
|
99
|
+
test_db = Member(db_path)
|
|
100
|
+
test_db.import_csv(mock_csv_file)
|
|
101
|
+
return test_db
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# --- Tests ---
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_empty_db(capsys):
|
|
108
|
+
"""
|
|
109
|
+
Test empty db
|
|
110
|
+
"""
|
|
111
|
+
with NamedTemporaryFile(suffix=".db") as tmp:
|
|
112
|
+
empty_db = Member(tmp.name)
|
|
113
|
+
# create tables so is empty database
|
|
114
|
+
empty_db._create_tables() # pylint: disable=protected-access
|
|
115
|
+
empty_db.show_table()
|
|
116
|
+
captured = capsys.readouterr()
|
|
117
|
+
assert "(No data)" in captured.out
|
|
118
|
+
assert empty_db.count() == 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_invalid_csv_path_message(tmp_path, db_path, capsys):
|
|
122
|
+
"""
|
|
123
|
+
Test import non existing csv file
|
|
124
|
+
"""
|
|
125
|
+
non_exist = Path(tmp_path, "non-exist.csv")
|
|
126
|
+
txn = Member(db_path)
|
|
127
|
+
txn.import_csv(non_exist)
|
|
128
|
+
# Capture stdout
|
|
129
|
+
captured = capsys.readouterr()
|
|
130
|
+
assert "CSV file not found" in captured.out
|
|
131
|
+
assert str(non_exist) in captured.out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_member_import_and_validation(member_db):
|
|
135
|
+
"""
|
|
136
|
+
Test importing valid/invalid members from CSV.
|
|
137
|
+
"""
|
|
138
|
+
# Valid inserts
|
|
139
|
+
assert member_db.get_number_first_last("john", "doe") == 1
|
|
140
|
+
assert member_db.get_number("Jane Smith") == 2
|
|
141
|
+
assert member_db.get_name(2) == "Jane Smith"
|
|
142
|
+
# Invalid member number
|
|
143
|
+
assert member_db.get_name(888) is None
|
|
144
|
+
|
|
145
|
+
# Should not be inserted due to duplicate membermojo ID
|
|
146
|
+
assert member_db.get_number_first_last("Emily", "Stone") is None
|
|
147
|
+
|
|
148
|
+
# Should not be inserted due to duplicate member_number
|
|
149
|
+
assert member_db.get_number("Sara Connor") is None
|
|
150
|
+
|
|
151
|
+
# Should not be inserted due to invalid title
|
|
152
|
+
assert member_db.get_number_first_last("Rick", "Grimes") is None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_show_table(member_db):
|
|
156
|
+
"""
|
|
157
|
+
Test the show table function
|
|
158
|
+
"""
|
|
159
|
+
# Should be equal as default show_table is 5 entries and member_db is 2
|
|
160
|
+
entries = member_db.count()
|
|
161
|
+
assert entries == 2
|
|
162
|
+
assert member_db.show_table() == member_db.show_table(100)
|
|
163
|
+
assert member_db.show_table(entries) == member_db.show_table(100)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_get_number_first_last_not_found_raises(member_db):
|
|
167
|
+
"""
|
|
168
|
+
Test found_error
|
|
169
|
+
"""
|
|
170
|
+
with pytest.raises(
|
|
171
|
+
ValueError, match=r"❌ Cannot find: John Snow in member database."
|
|
172
|
+
):
|
|
173
|
+
member_db.get_number_first_last("John", "Snow", found_error=True)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_get_number_first_last_more_names(member_db):
|
|
177
|
+
"""
|
|
178
|
+
Test logic for 3 names passed
|
|
179
|
+
"""
|
|
180
|
+
assert member_db.get_number("Dr Jane Smith") == 2
|
|
181
|
+
assert member_db.get_number("John Jojo Doe") == 1
|
|
182
|
+
with pytest.raises(ValueError) as exc_info:
|
|
183
|
+
member_db.get_number("Emily Sara", found_error=True)
|
|
184
|
+
|
|
185
|
+
assert "Cannot find" in str(exc_info.value)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Iteration tests for the Member class
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from memberjojo import Member, MemberData
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_member_iter(tmp_path):
|
|
10
|
+
"""
|
|
11
|
+
Test for iterating over the member data.
|
|
12
|
+
"""
|
|
13
|
+
# Prepare sample CSV data
|
|
14
|
+
sample_csv = tmp_path / "members.csv"
|
|
15
|
+
sample_data = [
|
|
16
|
+
{
|
|
17
|
+
"Member number": "11",
|
|
18
|
+
"Title": "Mr",
|
|
19
|
+
"First name": "Johnny",
|
|
20
|
+
"Last name": "Doe",
|
|
21
|
+
"membermojo ID": "8001",
|
|
22
|
+
"Short URL": "http://short.url/johnny",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"Member number": "12",
|
|
26
|
+
"Title": "Ms",
|
|
27
|
+
"First name": "Janice",
|
|
28
|
+
"Last name": "Smith",
|
|
29
|
+
"membermojo ID": "8002",
|
|
30
|
+
"Short URL": "http://short.url/janice",
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Write CSV file
|
|
35
|
+
with sample_csv.open("w", newline="", encoding="utf-8") as f:
|
|
36
|
+
writer = csv.DictWriter(f, fieldnames=sample_data[0].keys())
|
|
37
|
+
writer.writeheader()
|
|
38
|
+
writer.writerows(sample_data)
|
|
39
|
+
|
|
40
|
+
# Create DB path
|
|
41
|
+
db_path = tmp_path / "test_members.db"
|
|
42
|
+
|
|
43
|
+
# Instantiate Member and import CSV
|
|
44
|
+
members = Member(db_path)
|
|
45
|
+
members.import_csv(sample_csv)
|
|
46
|
+
|
|
47
|
+
# Collect members from iterator
|
|
48
|
+
iterated_members = list(members)
|
|
49
|
+
|
|
50
|
+
# Check that iteration yields correct number of members
|
|
51
|
+
assert len(iterated_members) == len(sample_data)
|
|
52
|
+
|
|
53
|
+
# Check that fields match for first member
|
|
54
|
+
first = iterated_members[0]
|
|
55
|
+
assert isinstance(first, MemberData)
|
|
56
|
+
assert first.member_num == int(sample_data[0]["Member number"])
|
|
57
|
+
assert first.title == sample_data[0]["Title"]
|
|
58
|
+
assert first.first_name == sample_data[0]["First name"]
|
|
59
|
+
assert first.last_name == sample_data[0]["Last name"]
|
|
60
|
+
assert first.membermojo_id == int(sample_data[0]["membermojo ID"])
|
|
61
|
+
assert first.short_url == sample_data[0]["Short URL"]
|
|
62
|
+
|
|
63
|
+
# Check second member also matches
|
|
64
|
+
second = iterated_members[1]
|
|
65
|
+
assert second.first_name == sample_data[1]["First name"]
|
|
66
|
+
assert second.last_name == sample_data[1]["Last name"]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the transaction module
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import tempfile
|
|
6
|
+
import csv
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from memberjojo import Transaction # Update with your actual module name
|
|
11
|
+
|
|
12
|
+
# pylint: disable=redefined-outer-name
|
|
13
|
+
# or pylint thinks fixtures are redined as function variables
|
|
14
|
+
# --- Fixtures & Helpers ---
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def csv_file(tmp_path):
|
|
19
|
+
"""
|
|
20
|
+
Temp csv file for testing
|
|
21
|
+
"""
|
|
22
|
+
path = tmp_path / "test_data.csv"
|
|
23
|
+
data = [
|
|
24
|
+
{"id": "1", "amount": "100.5", "desc": "Deposit"},
|
|
25
|
+
{"id": "2", "amount": "200", "desc": "Withdrawal"},
|
|
26
|
+
{"id": "3", "amount": "150", "desc": "Refund"},
|
|
27
|
+
{"id": "4", "amount": "175", "desc": None},
|
|
28
|
+
{"id": "5", "amount": "345", "desc": ""},
|
|
29
|
+
]
|
|
30
|
+
with path.open("w", newline="") as f:
|
|
31
|
+
writer = csv.DictWriter(f, fieldnames=["id", "amount", "desc"])
|
|
32
|
+
writer.writeheader()
|
|
33
|
+
writer.writerows(data)
|
|
34
|
+
return Path(path)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def db_path():
|
|
39
|
+
"""
|
|
40
|
+
Temp file for db connection
|
|
41
|
+
"""
|
|
42
|
+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
|
|
43
|
+
path = Path(tmp.name)
|
|
44
|
+
yield path
|
|
45
|
+
path.unlink()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def payment_db(db_path, csv_file):
|
|
50
|
+
"""
|
|
51
|
+
Test sqlite transaction database
|
|
52
|
+
"""
|
|
53
|
+
test_db = Transaction(db_path)
|
|
54
|
+
test_db.import_csv(csv_file, pk_column="id")
|
|
55
|
+
return test_db
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.parametrize(
|
|
59
|
+
"input_value, expected",
|
|
60
|
+
[
|
|
61
|
+
(None, "TEXT"),
|
|
62
|
+
("", "TEXT"),
|
|
63
|
+
("abc", "TEXT"),
|
|
64
|
+
("123", "INTEGER"),
|
|
65
|
+
("123.45", "REAL"),
|
|
66
|
+
(" 42 ", "INTEGER"), # whitespace-trimmed input
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# --- Tests ---
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_guess_type_various(input_value, expected):
|
|
75
|
+
"""
|
|
76
|
+
Test all the code paths in _guess_type
|
|
77
|
+
"""
|
|
78
|
+
txn = Transaction(":memory:")
|
|
79
|
+
assert txn._guess_type(input_value) == expected # pylint: disable=protected-access
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_empty_csv_import(tmp_path, db_path):
|
|
83
|
+
"""
|
|
84
|
+
Test importing empty and just header csv
|
|
85
|
+
"""
|
|
86
|
+
txn = Transaction(db_path)
|
|
87
|
+
empty_csv = tmp_path / "empty.csv"
|
|
88
|
+
empty_csv.write_text("", encoding="utf-8") # Fully empty
|
|
89
|
+
|
|
90
|
+
assert empty_csv.exists()
|
|
91
|
+
assert empty_csv.stat().st_size == 0
|
|
92
|
+
with pytest.raises(ValueError, match="CSV file is empty."):
|
|
93
|
+
txn.import_csv(empty_csv)
|
|
94
|
+
|
|
95
|
+
# OR with only headers
|
|
96
|
+
empty_csv.write_text("id,amount,desc\n", encoding="utf-8")
|
|
97
|
+
|
|
98
|
+
# Use it in your import
|
|
99
|
+
with pytest.raises(ValueError, match="CSV file is empty."):
|
|
100
|
+
txn.import_csv(empty_csv)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_create_table_with_default_pk(csv_file, db_path):
|
|
104
|
+
"""
|
|
105
|
+
Test _create_tables uses first column as primary key when pk_column is None.
|
|
106
|
+
"""
|
|
107
|
+
txn = Transaction(db_path, table_name="auto_pk_table")
|
|
108
|
+
txn.import_csv(Path(csv_file)) # No pk_column provided
|
|
109
|
+
|
|
110
|
+
# Validate schema — first column should be the primary key
|
|
111
|
+
txn.cursor.execute("PRAGMA table_info(auto_pk_table)")
|
|
112
|
+
schema = txn.cursor.fetchall()
|
|
113
|
+
pk_cols = [col["name"] for col in schema if col["pk"] == 1]
|
|
114
|
+
|
|
115
|
+
assert pk_cols == ["id"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_invalid_csv_path_message(tmp_path, db_path, capsys):
|
|
119
|
+
"""
|
|
120
|
+
Test import non existing csv file
|
|
121
|
+
"""
|
|
122
|
+
non_exist = Path(tmp_path, "non-exist.csv")
|
|
123
|
+
txn = Transaction(db_path)
|
|
124
|
+
txn.import_csv(non_exist)
|
|
125
|
+
# Capture stdout
|
|
126
|
+
captured = capsys.readouterr()
|
|
127
|
+
assert "CSV file not found" in captured.out
|
|
128
|
+
assert str(non_exist) in captured.out
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_type_mismatch_in_second_import_raises(tmp_path, db_path):
|
|
132
|
+
"""
|
|
133
|
+
Import valid CSV first. Then try a second CSV with invalid type, and ensure it raises.
|
|
134
|
+
"""
|
|
135
|
+
txn = Transaction(db_path, table_name="payments")
|
|
136
|
+
|
|
137
|
+
# First CSV: valid
|
|
138
|
+
valid_csv = tmp_path / "valid.csv"
|
|
139
|
+
valid_csv.write_text(
|
|
140
|
+
("id,amount,desc\n" + "1,100.0,Deposit\n" + "2,200.0,Withdrawal\n"),
|
|
141
|
+
encoding="utf-8",
|
|
142
|
+
)
|
|
143
|
+
txn.import_csv(valid_csv, pk_column="id")
|
|
144
|
+
|
|
145
|
+
# Second CSV: invalid amount type
|
|
146
|
+
invalid_csv = tmp_path / "invalid.csv"
|
|
147
|
+
invalid_csv.write_text(
|
|
148
|
+
(
|
|
149
|
+
"id,amount,desc\n"
|
|
150
|
+
+ "3,not_a_number,Invalid Amount\n"
|
|
151
|
+
+ "6,not_a_number,Invalid Amount\n"
|
|
152
|
+
+ "7,not_a_number1,Invalid Amount\n"
|
|
153
|
+
+ "8,not_a_number2,Invalid Amount\n"
|
|
154
|
+
+ "9,not_a_number3,Invalid Amount\n"
|
|
155
|
+
+ "10,not_a_number4,Invalid Amount\n"
|
|
156
|
+
),
|
|
157
|
+
encoding="utf-8",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Reuse the same txn instance so the schema remains
|
|
161
|
+
with pytest.raises(ValueError, match="Failed to import:"):
|
|
162
|
+
txn.import_csv(invalid_csv, pk_column="id")
|
|
163
|
+
|
|
164
|
+
# Ensure the invalid row was not inserted
|
|
165
|
+
assert txn.count() == 2
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_import_with_custom_primary_key(csv_file, db_path):
|
|
169
|
+
"""
|
|
170
|
+
test_import_with_custom_primary_key
|
|
171
|
+
"""
|
|
172
|
+
txn = Transaction(db_path, table_name="transactions")
|
|
173
|
+
|
|
174
|
+
# Import using 'amount' as the PK
|
|
175
|
+
txn.import_csv(Path(csv_file), pk_column="amount")
|
|
176
|
+
|
|
177
|
+
# Check that 'amount' is used as primary key
|
|
178
|
+
txn.cursor.execute("PRAGMA table_info(transactions)")
|
|
179
|
+
schema = txn.cursor.fetchall()
|
|
180
|
+
pk_columns = [col["name"] for col in schema if col["pk"] == 1]
|
|
181
|
+
assert pk_columns == ["amount"], "Expected 'amount' to be primary key"
|
|
182
|
+
|
|
183
|
+
# Check row count
|
|
184
|
+
assert txn.count() == 5
|
|
185
|
+
|
|
186
|
+
# Retrieve row by primary key
|
|
187
|
+
row = txn.get_row("amount", 200.0)
|
|
188
|
+
assert row is not None
|
|
189
|
+
assert row["desc"] == "Withdrawal"
|
|
190
|
+
assert row["id"] == 2
|
|
191
|
+
# Test null entry_value
|
|
192
|
+
assert txn.get_row("amount", "") is None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_import_missing_primary_key(csv_file, db_path):
|
|
196
|
+
"""
|
|
197
|
+
test_import_with_custom_primary_key
|
|
198
|
+
"""
|
|
199
|
+
txn = Transaction(db_path, table_name="transactions")
|
|
200
|
+
|
|
201
|
+
with pytest.raises(
|
|
202
|
+
ValueError, match="Primary key column 'none_id' not found in CSV."
|
|
203
|
+
):
|
|
204
|
+
# Import using 'none_id' as the PK
|
|
205
|
+
txn.import_csv(Path(csv_file), pk_column="none_id")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_duplicate_primary_key_ignored(payment_db, csv_file):
|
|
209
|
+
"""
|
|
210
|
+
test_duplicate_primary_key_ignored
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
# Re-import same CSV — should ignore duplicates due to OR IGNORE
|
|
214
|
+
payment_db.import_csv(Path(csv_file), pk_column="id")
|
|
215
|
+
|
|
216
|
+
assert payment_db.count() == 5 # No duplicates added
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_get_row_multi(payment_db):
|
|
220
|
+
"""
|
|
221
|
+
Test retrieving a row using multiple column conditions
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
# Exact match for id=2 and desc='Withdrawal'
|
|
225
|
+
row = payment_db.get_row_multi({"id": "2", "desc": "Withdrawal"})
|
|
226
|
+
assert row is not None
|
|
227
|
+
assert row["id"] == 2
|
|
228
|
+
assert row["desc"] == "Withdrawal"
|
|
229
|
+
assert row["amount"] == 200.0
|
|
230
|
+
|
|
231
|
+
# Match with numeric and empty string
|
|
232
|
+
row = payment_db.get_row_multi({"id": "5", "desc": ""})
|
|
233
|
+
assert row is not None
|
|
234
|
+
assert row["id"] == 5
|
|
235
|
+
assert row["desc"] is None
|
|
236
|
+
assert row["amount"] == 345.0
|
|
237
|
+
|
|
238
|
+
# No match
|
|
239
|
+
row = payment_db.get_row_multi({"id": "3", "desc": "Not a match"})
|
|
240
|
+
assert row is None
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test for _version.py not present
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import sys
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_version_fallback():
|
|
11
|
+
"""
|
|
12
|
+
Remove _version to test for missing _version.py
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Remove the _version module from sys.modules if present
|
|
16
|
+
sys.modules.pop("memberjojo._version", None)
|
|
17
|
+
|
|
18
|
+
# Patch sys.modules so importing memberjojo._version raises ImportError
|
|
19
|
+
with mock.patch.dict("sys.modules", {"memberjojo._version": None}):
|
|
20
|
+
# Remove the main package module so reload triggers fresh import logic
|
|
21
|
+
sys.modules.pop("memberjojo", None)
|
|
22
|
+
|
|
23
|
+
import memberjojo # pylint: disable=import-outside-toplevel
|
|
24
|
+
|
|
25
|
+
importlib.reload(memberjojo)
|
|
26
|
+
|
|
27
|
+
assert memberjojo.__version__ == "0.0.0+local"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|