memberjojo 2.3__tar.gz → 3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memberjojo
3
- Version: 2.3
3
+ Version: 3.0
4
4
  Summary: memberjojo - tools for working with members.
5
5
  Author-email: Duncan Bellamy <dunk@denkimushi.com>
6
6
  Requires-Python: >=3.8
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2.3'
32
- __version_tuple__ = version_tuple = (2, 3)
31
+ __version__ = version = '3.0'
32
+ __version_tuple__ = version_tuple = (3, 0)
33
33
 
34
- __commit_id__ = commit_id = 'g9a43ccd5d'
34
+ __commit_id__ = commit_id = 'g594a20473'
@@ -187,39 +187,58 @@ class MojoSkel:
187
187
  # Cleanup old table (always)
188
188
  self.conn.execute(f"DROP TABLE {old_table}")
189
189
 
190
- def download_csv(self, session: requests.Session, url: str):
190
+ def download_csv(self, session: requests.Session, url: str, merge: bool = False):
191
191
  """
192
192
  Download the CSV from url and import into the sqlite database
193
193
  If a previous table exists, generate a diff
194
194
 
195
195
  :param session: Requests session to use for download
196
196
  :param url: url of the csv to download
197
+ :param merge: (optional) If True, merge into existing table. Defaults to False.
197
198
  """
198
- had_existing = self.table_exists()
199
- old_table = self.rename_old_table(had_existing)
199
+ had_existing = False
200
+ old_table = ""
201
+
202
+ if not merge:
203
+ had_existing = self.table_exists()
204
+ old_table = self.rename_old_table(had_existing)
200
205
 
201
206
  # Download CSV as new table
202
- mojo_loader.download_csv_helper(self.conn, self.table_name, url, session)
207
+ mojo_loader.download_csv_helper(
208
+ self.conn, self.table_name, url, session, merge=merge
209
+ )
203
210
  self.row_class = self._build_dataclass_from_table()
204
211
 
212
+ if merge:
213
+ return
214
+
205
215
  if not had_existing:
206
216
  return
207
217
 
208
218
  self.print_diff(old_table)
209
219
 
210
- def import_csv(self, csv_path: Path):
220
+ def import_csv(self, csv_path: Path, merge: bool = False):
211
221
  """
212
222
  Import the passed CSV into the encrypted sqlite database
213
223
 
214
224
  :param csv_path: Path like path of csv file
225
+ :param merge: (optional) If True, merge into existing table. Defaults to False.
226
+ Form importing current, and expired members as headings are the same.
215
227
  """
216
- had_existing = self.table_exists()
217
- old_table = self.rename_old_table(had_existing)
228
+ had_existing = False
229
+ old_table = ""
230
+
231
+ if not merge:
232
+ had_existing = self.table_exists()
233
+ old_table = self.rename_old_table(had_existing)
218
234
 
219
235
  # Import CSV as new table
220
- mojo_loader.import_csv_helper(self.conn, self.table_name, csv_path)
236
+ mojo_loader.import_csv_helper(self.conn, self.table_name, csv_path, merge=merge)
221
237
  self.row_class = self._build_dataclass_from_table()
222
238
 
239
+ if merge:
240
+ return
241
+
223
242
  if not had_existing:
224
243
  return
225
244
 
@@ -257,7 +276,7 @@ class MojoSkel:
257
276
  self, entry_name: str, entry_value: str, only_one: bool = True
258
277
  ) -> Union[sqlite3.Row, List[sqlite3.Row], None]:
259
278
  """
260
- Retrieve a single row matching column = value (case-insensitive)
279
+ Retrieve a single or multiple rows matching column = value (case-insensitive)
261
280
 
262
281
  :param entry_name: Column name to filter by
263
282
  :param entry_value: Value to match
@@ -335,12 +354,19 @@ class MojoSkel:
335
354
  self.cursor.execute(base_query, values)
336
355
  return [self._row_to_obj(row) for row in self.cursor.fetchall()]
337
356
 
357
+ def run_count_query(self, sql: str, params: tuple):
358
+ """
359
+ Generate whole sql query for running on db table for counting and run
360
+
361
+ :param sql: the sqlite query for matching rows
362
+ :param params: the paramaters to use for the query
363
+ """
364
+ sql_query = f"SELECT count (*) FROM {self.table_name} {sql}"
365
+ cursor = self.cursor.execute(sql_query, params)
366
+ return cursor.fetchone()[0]
367
+
338
368
  def table_exists(self) -> bool:
339
369
  """
340
370
  Return True or False if a table exists
341
371
  """
342
- self.cursor.execute(
343
- "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1;",
344
- (self.table_name,),
345
- )
346
- return self.cursor.fetchone() is not None
372
+ return mojo_loader.table_exists(self.cursor, self.table_name)
@@ -125,28 +125,45 @@ def _create_table_from_columns(table_name: str, columns: dict[str, str]) -> str:
125
125
  )
126
126
 
127
127
 
128
+ def table_exists(cursor, table_name: str) -> bool:
129
+ """
130
+ Return True or False if a table exists
131
+
132
+ :param cursor: SQLite cursor of db to find table in
133
+ :param table_name: name of the table to check existance of
134
+
135
+ :return: bool of existence
136
+ """
137
+ cursor.execute(
138
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1;",
139
+ (table_name,),
140
+ )
141
+ return cursor.fetchone() is not None
142
+
143
+
128
144
  # -----------------------
129
145
  # CSV Import
130
146
  # -----------------------
131
147
 
132
148
 
133
- def import_data(conn, table_name: str, reader: DictReader):
149
+ def import_data(conn, table_name: str, reader: DictReader, merge: bool = False):
134
150
  """
135
151
  Import data in the DictReader into the SQLite3 database at conn
136
152
 
137
153
  :param conn: SQLite database connection to use
138
154
  :param table_name: Name of the table to import into
139
155
  :param reader: A Dictreader object to import from
156
+ :param merge: (optional) If True, merge into existing table. Defaults to False.
140
157
  """
141
- inferred_cols = infer_columns_from_rows(reader)
142
158
 
143
159
  cursor = conn.cursor()
144
- # Drop existing table
145
- cursor.execute(f'DROP TABLE IF EXISTS "{table_name}";')
146
-
147
- # Create table
148
- create_sql = _create_table_from_columns(table_name, inferred_cols)
149
- cursor.execute(create_sql)
160
+ if not merge or not table_exists(cursor, table_name):
161
+ # Drop existing table
162
+ cursor.execute(f'DROP TABLE IF EXISTS "{table_name}";')
163
+ inferred_cols = infer_columns_from_rows(reader)
164
+ # Create table
165
+ create_sql = _create_table_from_columns(table_name, inferred_cols)
166
+ cursor.execute(create_sql)
150
167
 
151
168
  # Insert rows
152
169
  cols = list(reader[0].keys())
@@ -163,7 +180,7 @@ def import_data(conn, table_name: str, reader: DictReader):
163
180
  conn.commit()
164
181
 
165
182
 
166
- def import_csv_helper(conn, table_name: str, csv_path: Path):
183
+ def import_csv_helper(conn, table_name: str, csv_path: Path, merge: bool = False):
167
184
  """
168
185
  Import CSV into database using given cursor
169
186
  Column types inferred automatically
@@ -171,6 +188,7 @@ def import_csv_helper(conn, table_name: str, csv_path: Path):
171
188
  :param conn: SQLite database connection to use
172
189
  :param table_name: Table to import the CSV into
173
190
  :param csv_path: Path like path of the CSV file to import
191
+ :param merge: (optional) If True, merge into existing table. Defaults to False.
174
192
  """
175
193
  if not csv_path.exists():
176
194
  raise FileNotFoundError(f"CSV file not found: {csv_path}")
@@ -180,7 +198,7 @@ def import_csv_helper(conn, table_name: str, csv_path: Path):
180
198
  reader = list(DictReader(f))
181
199
  if not reader:
182
200
  raise ValueError("CSV file is empty.")
183
- import_data(conn, table_name, reader)
201
+ import_data(conn, table_name, reader, merge=merge)
184
202
 
185
203
 
186
204
  # -----------------------
@@ -350,7 +368,7 @@ def _generate_sql_diff(
350
368
 
351
369
 
352
370
  def download_csv_helper(
353
- conn, table_name: str, url: str, session: requests.Session
371
+ conn, table_name: str, url: str, session: requests.Session, merge: bool = False
354
372
  ) -> IO[str]:
355
373
  """
356
374
  Download url into a StringIO file object using streaming
@@ -360,9 +378,10 @@ def download_csv_helper(
360
378
  :param table_name: The name of the table to import it into
361
379
  :param url: URL of the csv to download
362
380
  :param session: A requests session to use for the download
381
+ :param merge: (optional) If True, merge into existing table. Defaults to False.
363
382
  """
364
383
 
365
- print(f"Downloading from: {url}")
384
+ print(f"☁️ Downloading from: {url}")
366
385
 
367
386
  # Enable streaming
368
387
  with session.get(url, stream=True) as resp:
@@ -381,4 +400,4 @@ def download_csv_helper(
381
400
  string_buffer.seek(0)
382
401
 
383
402
  print(f"✅ Downloaded with encoding {resp.encoding}.")
384
- import_data(conn, table_name, list(DictReader(string_buffer)))
403
+ import_data(conn, table_name, list(DictReader(string_buffer)), merge=merge)
@@ -63,6 +63,19 @@ class Member(MojoSkel):
63
63
  value = row[0]
64
64
  return str(value).lower() == "yes"
65
65
 
66
+ def member_type_count(self, membership_type: str):
67
+ """
68
+ Count members by membership type string
69
+
70
+ :param membership_type: the string to match, can use percent to match
71
+ remaining or preceeding text
72
+ - Full (match only Full)
73
+ - Full% (match Full and any words after)
74
+ - %Full% ( match Full in the middle)
75
+ """
76
+ query = "WHERE membership LIKE ?"
77
+ return self.run_count_query(query, (f"{membership_type}",))
78
+
66
79
  def get_number_first_last(
67
80
  self, first_name: str, last_name: str, found_error: bool = False
68
81
  ) -> Optional[int]:
File without changes
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Merge import tests for MojoSkel
4
+ """
5
+
6
+ from memberjojo.mojo_common import MojoSkel
7
+
8
+ from .utils import write_csv
9
+
10
+
11
+ def test_import_merge_csv(tmp_path):
12
+ """
13
+ Test importing one CSV, then merging a second CSV,
14
+ and verify that it appends instead of replacing.
15
+ """
16
+
17
+ # 1. Original CSV
18
+ csv1 = tmp_path / "members1.csv"
19
+ original_rows = [
20
+ {"id": "1", "name": "Alice", "age": "30"},
21
+ ]
22
+ write_csv(csv1, original_rows)
23
+
24
+ # Create DB
25
+ db_path = tmp_path / "test_merge.db"
26
+ password = "Needs a Password"
27
+
28
+ # Load initial CSV
29
+ m = MojoSkel(str(db_path), password, "members")
30
+ m.import_csv(csv1)
31
+
32
+ assert m.count() == 1
33
+
34
+ # 2. Second CSV to merge
35
+ csv2 = tmp_path / "members2.csv"
36
+ new_rows = [
37
+ {"id": "2", "name": "Bob", "age": "40"},
38
+ ]
39
+ write_csv(csv2, new_rows)
40
+
41
+ # Import with merge=True
42
+ m.import_csv(csv2, merge=True)
43
+
44
+ # Should have 2 rows now
45
+ assert m.count() == 2
46
+
47
+ # Verify content
48
+ # We iterate over m which yields row objects
49
+ rows = list(m)
50
+ assert len(rows) == 2
51
+ names = {r.name for r in rows}
52
+ assert "Alice" in names
53
+ assert "Bob" in names
54
+
55
+
56
+ def test_import_merge_csv_no_existing(tmp_path):
57
+ """
58
+ Test merging into a non-existent table (should act like create).
59
+ """
60
+ csv1 = tmp_path / "members_new.csv"
61
+ rows = [{"id": "1", "name": "Alice", "age": "30"}]
62
+ write_csv(csv1, rows)
63
+
64
+ db_path = tmp_path / "test_merge_new.db"
65
+ password = "Needs a Password"
66
+
67
+ m = MojoSkel(str(db_path), password, "members")
68
+ # Table doesn't exist yet
69
+ assert not m.table_exists()
70
+
71
+ m.import_csv(csv1, merge=True)
72
+
73
+ assert m.table_exists()
74
+ assert m.count() == 1
75
+ row = list(m)[0]
76
+ assert row.name == "Alice"
@@ -3,18 +3,9 @@
3
3
  CSV diff tests for the Member class
4
4
  """
5
5
 
6
- import csv
7
- from pathlib import Path
8
-
9
6
  from memberjojo.mojo_common import MojoSkel
10
7
 
11
-
12
- def write_csv(path: Path, rows: list[dict]):
13
- """Utility: write CSV with DictWriter."""
14
- with path.open("w", newline="", encoding="utf-8") as f:
15
- writer = csv.DictWriter(f, fieldnames=rows[0].keys())
16
- writer.writeheader()
17
- writer.writerows(rows)
8
+ from .utils import write_csv
18
9
 
19
10
 
20
11
  def test_import_new_csv_generates_diff(tmp_path, capsys):
@@ -25,6 +25,7 @@ def mock_csv_file(tmp_path):
25
25
  "Title",
26
26
  "First name",
27
27
  "Last name",
28
+ "Membership",
28
29
  "membermojo ID",
29
30
  "Short URL",
30
31
  "Active Member",
@@ -36,6 +37,7 @@ def mock_csv_file(tmp_path):
36
37
  "Title": "Mr",
37
38
  "First name": "John",
38
39
  "Last name": "Doe",
40
+ "Membership": "Full",
39
41
  "membermojo ID": "1001",
40
42
  "Short URL": "http://short.url/johndoe",
41
43
  "Active Member": "yes",
@@ -46,6 +48,7 @@ def mock_csv_file(tmp_path):
46
48
  "Title": "Ms",
47
49
  "First name": "Jane",
48
50
  "Last name": "Smith",
51
+ "Membership": "Full",
49
52
  "membermojo ID": "1002",
50
53
  "Short URL": "http://short.url/janesmith",
51
54
  "Active Member": "no",
@@ -56,6 +59,7 @@ def mock_csv_file(tmp_path):
56
59
  "Title": "Dr",
57
60
  "First name": "Emily",
58
61
  "Last name": "Stone",
62
+ "Membership": "Full",
59
63
  "membermojo ID": "1001",
60
64
  "Short URL": "http://short.url/emilystone",
61
65
  "Active Member": "yes",
@@ -66,6 +70,7 @@ def mock_csv_file(tmp_path):
66
70
  "Title": "Mrs",
67
71
  "First name": "Sara",
68
72
  "Last name": "Connor",
73
+ "Membership": "Half",
69
74
  "membermojo ID": "1003",
70
75
  "Short URL": "http://short.url/saraconnor",
71
76
  "Active Member": "no",
@@ -76,6 +81,7 @@ def mock_csv_file(tmp_path):
76
81
  "Title": "Sir",
77
82
  "First name": "Rick",
78
83
  "Last name": "Grimes",
84
+ "Membership": "Half",
79
85
  "membermojo ID": "1004",
80
86
  "Short URL": "http://short.url/rickgrimes",
81
87
  "Active Member": "yes",
@@ -267,3 +273,11 @@ def test_get_fuzz_name_raises_value_error_when_no_match_and_found_error_true(mem
267
273
  match=r"❌ Cannot find xyz123notfound in member database with fuzzy match\.",
268
274
  ):
269
275
  member_db.get_fuzz_name("xyz123notfound", found_error=True)
276
+
277
+
278
+ def test_member_count(member_db):
279
+ """
280
+ Test member_type_count returns the correct value
281
+ """
282
+ assert member_db.member_type_count("half") == 2
283
+ assert member_db.member_type_count("noop") == 0
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env python3
2
+ """Utils for tests"""
3
+
4
+ import csv
5
+ from pathlib import Path
6
+
7
+
8
+ def write_csv(path: Path, rows: list[dict]):
9
+ """Utility: write CSV with DictWriter."""
10
+ with path.open("w", newline="", encoding="utf-8") as f:
11
+ writer = csv.DictWriter(f, fieldnames=rows[0].keys())
12
+ writer.writeheader()
13
+ writer.writerows(rows)
File without changes
File without changes
File without changes
File without changes
File without changes