memberjojo 2.1__py3-none-any.whl → 2.3__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.
memberjojo/mojo_loader.py CHANGED
@@ -1,12 +1,33 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Helper module for importing a CSV into a SQLite database.
3
+ Helper module for importing a CSV into a SQLite database
4
4
  """
5
5
 
6
+ from collections import defaultdict, Counter
6
7
  from csv import DictReader
8
+ from dataclasses import dataclass
9
+ from io import StringIO
7
10
  from pathlib import Path
11
+ from typing import Any, IO, Tuple
12
+
8
13
  import re
9
- from collections import defaultdict, Counter
14
+ import sqlite3 as sqlite3_builtin
15
+
16
+ import requests
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DiffRow:
21
+ """
22
+ Represents a single diff result
23
+
24
+ - diff_type: 'added' | 'deleted' | 'changed'
25
+ - preview: tuple of values, with preview[0] == key
26
+ """
27
+
28
+ diff_type: str
29
+ preview: Tuple[Any, ...]
30
+
10
31
 
11
32
  # -----------------------
12
33
  # Normalization & Type Guessing
@@ -15,11 +36,11 @@ from collections import defaultdict, Counter
15
36
 
16
37
  def _normalize(name: str) -> str:
17
38
  """
18
- Normalize a column name: lowercase, remove symbols, convert to snake case.
39
+ Normalize a column name: lowercase, remove symbols, convert to snake case
19
40
 
20
- :param name: Raw name to normalize.
41
+ :param name: Raw name to normalize
21
42
 
22
- :return: Normalized lowercase string in snake case with no symbols.
43
+ :return: Normalized lowercase string in snake case with no symbols
23
44
  """
24
45
  name = name.strip().lower()
25
46
  name = re.sub(r"[^a-z0-9]+", "_", name)
@@ -28,7 +49,11 @@ def _normalize(name: str) -> str:
28
49
 
29
50
  def _guess_type(value: any) -> str:
30
51
  """
31
- Guess SQLite data type of a CSV value: 'INTEGER', 'REAL', or 'TEXT'.
52
+ Guess SQLite data type of a CSV value: 'INTEGER', 'REAL', or 'TEXT'
53
+
54
+ :param value: entry from sqlite database to guess the type of
55
+
56
+ :return: string of the type, TEXT, INTEGER, REAL
32
57
  """
33
58
  if value is None:
34
59
  return "TEXT"
@@ -49,8 +74,12 @@ def _guess_type(value: any) -> str:
49
74
 
50
75
  def infer_columns_from_rows(rows: list[dict]) -> dict[str, str]:
51
76
  """
52
- Infer column types from CSV rows.
53
- Returns mapping: normalized column name -> SQLite type.
77
+ Infer column types from CSV rows
78
+ Returns mapping: normalized column name -> SQLite type
79
+
80
+ :param rows: list of rows to use for inference
81
+
82
+ :return: dict of name, type for columns
54
83
  """
55
84
  type_counters = defaultdict(Counter)
56
85
 
@@ -78,18 +107,21 @@ def infer_columns_from_rows(rows: list[dict]) -> dict[str, str]:
78
107
 
79
108
  def _create_table_from_columns(table_name: str, columns: dict[str, str]) -> str:
80
109
  """
81
- Generate CREATE TABLE SQL from column type mapping.
110
+ Generate CREATE TABLE SQL from column type mapping
111
+ Adds an auto-incrementing rowid as the primary key
82
112
 
83
- :param table_name: Table to use when creating columns.
84
- :param columns: dict of columns to create.
113
+ :param table_name: Table to use when creating columns
114
+ :param columns: dict of columns to create
85
115
 
86
- :return: SQL commands to create the table.
116
+ :return: SQL commands to create the table
87
117
  """
88
- column_defs = [f'"{col}" {col_type}' for col, col_type in columns.items()]
118
+ col_defs = ["rowid INTEGER PRIMARY KEY AUTOINCREMENT"]
119
+
120
+ for col, col_type in columns.items():
121
+ col_defs.append(f'"{col}" {col_type}')
122
+
89
123
  return (
90
- f'CREATE TABLE IF NOT EXISTS "{table_name}" (\n'
91
- + ",\n".join(column_defs)
92
- + "\n)"
124
+ f'CREATE TABLE IF NOT EXISTS "{table_name}" (\n' + ",\n".join(col_defs) + "\n)"
93
125
  )
94
126
 
95
127
 
@@ -98,14 +130,47 @@ def _create_table_from_columns(table_name: str, columns: dict[str, str]) -> str:
98
130
  # -----------------------
99
131
 
100
132
 
133
+ def import_data(conn, table_name: str, reader: DictReader):
134
+ """
135
+ Import data in the DictReader into the SQLite3 database at conn
136
+
137
+ :param conn: SQLite database connection to use
138
+ :param table_name: Name of the table to import into
139
+ :param reader: A Dictreader object to import from
140
+ """
141
+ inferred_cols = infer_columns_from_rows(reader)
142
+
143
+ 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)
150
+
151
+ # Insert rows
152
+ cols = list(reader[0].keys())
153
+ norm_map = {c: _normalize(c) for c in cols}
154
+ colnames = ",".join(f'"{norm_map[c]}"' for c in cols)
155
+ placeholders = ",".join("?" for _ in cols)
156
+ insert_sql = f'INSERT INTO "{table_name}" ({colnames}) VALUES ({placeholders})'
157
+
158
+ for row in reader:
159
+ values = [row[c] if row[c] != "" else None for c in cols]
160
+ cursor.execute(insert_sql, values)
161
+
162
+ cursor.close()
163
+ conn.commit()
164
+
165
+
101
166
  def import_csv_helper(conn, table_name: str, csv_path: Path):
102
167
  """
103
- Import CSV into database using given cursor.
104
- Column types inferred automatically.
168
+ Import CSV into database using given cursor
169
+ Column types inferred automatically
105
170
 
106
- :param conn: SQLite database connection to use.
107
- :param table_name: Table to import the CSV into.
108
- :param csv_path: Path like path of the CSV file to import.
171
+ :param conn: SQLite database connection to use
172
+ :param table_name: Table to import the CSV into
173
+ :param csv_path: Path like path of the CSV file to import
109
174
  """
110
175
  if not csv_path.exists():
111
176
  raise FileNotFoundError(f"CSV file not found: {csv_path}")
@@ -115,26 +180,205 @@ def import_csv_helper(conn, table_name: str, csv_path: Path):
115
180
  reader = list(DictReader(f))
116
181
  if not reader:
117
182
  raise ValueError("CSV file is empty.")
118
- inferred_cols = infer_columns_from_rows(reader)
183
+ import_data(conn, table_name, reader)
184
+
185
+
186
+ # -----------------------
187
+ # diff generation
188
+ # -----------------------
189
+
190
+
191
+ def _diffrow_from_sql_row(row: sqlite3_builtin.Row) -> DiffRow:
192
+ """
193
+ Convert a sqlite3.Row from generate_sql_diff into DiffRow
194
+ Row shape:
195
+ (diff_type, col1, col2, col3, ...)
119
196
 
120
- cursor = conn.cursor()
121
- # Drop existing table
122
- cursor.execute(f'DROP TABLE IF EXISTS "{table_name}";')
197
+ :param row: Row from sqlite3 database to create a dataclass entry from
123
198
 
124
- # Create table
125
- create_sql = _create_table_from_columns(table_name, inferred_cols)
126
- cursor.execute(create_sql)
199
+ :return: A dataclass of the row
200
+ """
201
+ return DiffRow(
202
+ diff_type=row[0],
203
+ preview=tuple(row[1:]),
204
+ )
127
205
 
128
- # Insert rows
129
- cols = list(reader[0].keys())
130
- norm_map = {c: _normalize(c) for c in cols}
131
- colnames = ",".join(f'"{norm_map[c]}"' for c in cols)
132
- placeholders = ",".join("?" for _ in cols)
133
- insert_sql = f'INSERT INTO "{table_name}" ({colnames}) VALUES ({placeholders})'
134
206
 
135
- for row in reader:
136
- values = [row[c] if row[c] != "" else None for c in cols]
137
- cursor.execute(insert_sql, values)
207
+ def diff_cipher_tables(
208
+ conn,
209
+ *,
210
+ new_table: str,
211
+ old_table: str,
212
+ ) -> list[DiffRow]:
213
+ """
214
+ Copy old and new tables from SQLCipher into a single
215
+ in-memory sqlite3 database and diff them there.
138
216
 
139
- cursor.close()
140
- conn.commit()
217
+ :param conn: sqlite connection to the db
218
+ :param new_table: name of the new table for comparison
219
+ :param old_table: name of the old table for comparison
220
+
221
+ :return: a list of DiffRow entries of the changed rows
222
+ """
223
+
224
+ plain = sqlite3_builtin.connect(":memory:")
225
+ plain.row_factory = sqlite3_builtin.Row
226
+
227
+ try:
228
+ for table in (old_table, new_table):
229
+ # Clone schema using SQLite itself
230
+ schema_sql = conn.execute(
231
+ """
232
+ SELECT sql
233
+ FROM sqlite_master
234
+ WHERE type='table' AND name=?
235
+ """,
236
+ (table,),
237
+ ).fetchone()
238
+
239
+ if schema_sql is None:
240
+ raise RuntimeError(f"Table {table!r} not found in cipher DB")
241
+
242
+ plain.execute(schema_sql[0])
243
+
244
+ # 2. Copy data
245
+ rows = conn.execute(f"SELECT * FROM {table}")
246
+ cols = [d[0] for d in rows.description]
247
+
248
+ col_list = ", ".join(cols)
249
+ placeholders = ", ".join("?" for _ in cols)
250
+
251
+ plain.executemany(
252
+ f"INSERT INTO {table} ({col_list}) VALUES ({placeholders})",
253
+ rows.fetchall(),
254
+ )
255
+
256
+ # 3. Run sqlite-only diff
257
+ rows = _generate_sql_diff(
258
+ plain,
259
+ new_table=new_table,
260
+ old_table=old_table,
261
+ )
262
+
263
+ return [_diffrow_from_sql_row(r) for r in rows]
264
+
265
+ finally:
266
+ plain.close()
267
+
268
+
269
+ def _generate_sql_diff(
270
+ conn: sqlite3_builtin.Connection,
271
+ *,
272
+ new_table: str,
273
+ old_table: str,
274
+ ) -> list[sqlite3_builtin.Row]:
275
+ """
276
+ Generate a diff between two tables using standard SQLite features
277
+
278
+ - Uses rowid as the primary key for joining
279
+ - Returned row shape:
280
+ (diff_type, preview_col1, preview_col2, preview_col3, ...)
281
+
282
+ :param conn: sqlite connection to the db, using python builtin sqlite
283
+ :param new_table: name of the new table to use for comparison
284
+ :param old_table: name of the old table to use for comparison
285
+
286
+ :return: list of sqlite rows that are changed
287
+ """
288
+
289
+ # Introspect schema (order-preserving)
290
+ cols_info = conn.execute(f"PRAGMA table_info({new_table})").fetchall()
291
+
292
+ if not cols_info:
293
+ raise RuntimeError(f"Table {new_table!r} has no columns")
294
+
295
+ cols = [row[1] for row in cols_info]
296
+
297
+ # Exclude rowid from consideration for key or data
298
+ main_cols = [c for c in cols if c != "rowid"]
299
+
300
+ # First column is key, others are for comparison
301
+ key = main_cols[0]
302
+ non_key_cols = main_cols[1:]
303
+
304
+ # Preview columns (key first, then others for readability)
305
+ preview_cols = [key] + non_key_cols[:5]
306
+
307
+ new_preview = ", ".join(f"n.{c}" for c in preview_cols)
308
+ old_preview = ", ".join(f"o.{c}" for c in preview_cols)
309
+
310
+ # Row-value comparison (NULL-safe)
311
+ if non_key_cols:
312
+ changed_predicate = (
313
+ f"({', '.join(f'n.{c}' for c in non_key_cols)}) "
314
+ f"IS NOT "
315
+ f"({', '.join(f'o.{c}' for c in non_key_cols)})"
316
+ )
317
+ else:
318
+ # Key-only table
319
+ changed_predicate = "0"
320
+
321
+ sql = f"""
322
+ WITH
323
+ added AS (
324
+ SELECT 'added' AS diff_type, {new_preview}
325
+ FROM {new_table} n
326
+ LEFT JOIN {old_table} o USING ({key})
327
+ WHERE o.{key} IS NULL
328
+ ),
329
+ deleted AS (
330
+ SELECT 'deleted' AS diff_type, {old_preview}
331
+ FROM {old_table} o
332
+ LEFT JOIN {new_table} n USING ({key})
333
+ WHERE n.{key} IS NULL
334
+ ),
335
+ changed AS (
336
+ SELECT 'changed' AS diff_type, {new_preview}
337
+ FROM {new_table} n
338
+ JOIN {old_table} o USING ({key})
339
+ WHERE {changed_predicate}
340
+ )
341
+ SELECT * FROM added
342
+ UNION ALL
343
+ SELECT * FROM deleted
344
+ UNION ALL
345
+ SELECT * FROM changed
346
+ ORDER BY {key};
347
+ """
348
+
349
+ return list(conn.execute(sql))
350
+
351
+
352
+ def download_csv_helper(
353
+ conn, table_name: str, url: str, session: requests.Session
354
+ ) -> IO[str]:
355
+ """
356
+ Download url into a StringIO file object using streaming
357
+ and import into database
358
+
359
+ :param conn: The SQLite3 database connection to use
360
+ :param table_name: The name of the table to import it into
361
+ :param url: URL of the csv to download
362
+ :param session: A requests session to use for the download
363
+ """
364
+
365
+ print(f"Downloading from: {url}")
366
+
367
+ # Enable streaming
368
+ with session.get(url, stream=True) as resp:
369
+ resp.raise_for_status()
370
+
371
+ # Initialize the string buffer
372
+ string_buffer = StringIO()
373
+
374
+ # Stream decoded text
375
+ # decode_unicode=True uses the encoding from the response headers
376
+ for chunk in resp.iter_content(chunk_size=8192, decode_unicode=True):
377
+ if chunk:
378
+ string_buffer.write(chunk)
379
+
380
+ # Reset pointer to the beginning for DictReader
381
+ string_buffer.seek(0)
382
+
383
+ print(f"✅ Downloaded with encoding {resp.encoding}.")
384
+ import_data(conn, table_name, list(DictReader(string_buffer)))
memberjojo/mojo_member.py CHANGED
@@ -1,25 +1,28 @@
1
1
  """
2
- Member module for creating and interacting with a SQLite database.
2
+ Member module for creating and interacting with a SQLite database
3
3
 
4
4
  This module loads data from a `members.csv` file downloaded from Membermojo,
5
- stores it in SQLite, and provides helper functions for member lookups.
5
+ stores it in SQLite, and provides helper functions for member lookups
6
6
  """
7
7
 
8
+ from difflib import get_close_matches
8
9
  from pathlib import Path
10
+ from pprint import pprint
9
11
  from typing import Optional
10
12
  from .mojo_common import MojoSkel
11
13
 
12
14
 
13
15
  class Member(MojoSkel):
14
16
  """
15
- Subclass of MojoSkel providing member-specific database functions.
17
+ Subclass of MojoSkel providing member-specific database functions
16
18
 
17
19
  This class connects to a SQLite database and supports importing member data
18
- from CSV and performing queries like lookup by name or member number.
20
+ from CSV and performing queries like lookup by name or member number
19
21
 
20
- :param member_db_path (Path): Path to the SQLite database file.
21
- :param table_name (str): (optional) Table name to use. Defaults to "members".
22
- :param db_key: (optional) key to unlock the encrypted sqlite database, unencrypted if unset.
22
+ :param member_db_path (Path): Path to the SQLite database file
23
+ :param db_key: key to unlock the encrypted sqlite database,
24
+ unencrypted if sqlcipher3 not installed or unset
25
+ :param table_name (str): (optional) Table name to use. Defaults to "members"
23
26
  """
24
27
 
25
28
  def __init__(
@@ -29,23 +32,50 @@ class Member(MojoSkel):
29
32
  table_name: str = "members",
30
33
  ):
31
34
  """
32
- Initialize the Member database handler.
35
+ Initialize the Member database handler
33
36
  """
34
37
  super().__init__(member_db_path, db_key, table_name)
35
38
 
39
+ def get_bool(self, entry_name: str, member_number: int) -> bool:
40
+ """
41
+ Return a bool for a member entry that is a tick box on membermojo
42
+
43
+ :param entry_name: The entry name to return as a bool
44
+ :param member_number: The member number to check value of entry_name
45
+
46
+ :return: True is entry is yes otherwise False
47
+
48
+ :raises ValueError: If entry name not found
49
+ """
50
+ sql = f"""
51
+ SELECT "{entry_name}"
52
+ FROM "{self.table_name}"
53
+ WHERE "member_number" = ?
54
+ """
55
+ self.cursor.execute(sql, (member_number,))
56
+
57
+ row = self.cursor.fetchone()
58
+ if row is None:
59
+ raise ValueError(
60
+ f"❌ Cannot find: {entry_name} for member {member_number}."
61
+ )
62
+
63
+ value = row[0]
64
+ return str(value).lower() == "yes"
65
+
36
66
  def get_number_first_last(
37
67
  self, first_name: str, last_name: str, found_error: bool = False
38
68
  ) -> Optional[int]:
39
69
  """
40
- Find a member number based on first and last name (case-insensitive).
70
+ Find a member number based on first and last name (case-insensitive)
41
71
 
42
- :param first_name: First name of the member.
43
- :param last_name: Last name of the member.
44
- :param found_error: (optional): If True, raises ValueError if not found.
72
+ :param first_name: First name of the member
73
+ :param last_name: Last name of the member
74
+ :param found_error: (optional): If True, raises ValueError if not found
45
75
 
46
- :return: The member number if found, otherwise None.
76
+ :return: The member number if found, otherwise None
47
77
 
48
- :raises ValueError: If not found and `found_error` is True.
78
+ :raises ValueError: If not found and `found_error` is True
49
79
  """
50
80
  sql = f"""
51
81
  SELECT "member_number"
@@ -54,6 +84,12 @@ class Member(MojoSkel):
54
84
  """
55
85
  self.cursor.execute(sql, (first_name, last_name))
56
86
  result = self.cursor.fetchone()
87
+ if self.debug:
88
+ print("Sql:")
89
+ pprint(sql)
90
+ print(f"First Name: {first_name} Last Name: {last_name}")
91
+ print("Result:")
92
+ pprint(result[0])
57
93
 
58
94
  if not result and found_error:
59
95
  raise ValueError(
@@ -64,18 +100,22 @@ class Member(MojoSkel):
64
100
 
65
101
  def get_number(self, full_name: str, found_error: bool = False) -> Optional[int]:
66
102
  """
67
- Find a member number by passed full_name.
103
+ Find a member number by passed full_name
68
104
  Tries first and last, and then middle last if 3 words,
69
- Then initial of first name if initials passed.
105
+ Then initial of first name if initials passed
106
+ Finnaly a fuzzy lookup is tried
70
107
 
71
- :param full_name: Full name of the member.
72
- :param found_error: (optional) Raise ValueError if not found.
108
+ :param full_name: Full name of the member
109
+ :param found_error: (optional) Raise ValueError if not found
73
110
 
74
- :return: Member number if found, else None.
111
+ :return: Member number if found, else None
75
112
 
76
- :raises ValueError: If not found and `found_error` is True.
113
+ :raises ValueError: If not found and `found_error` is True
77
114
  """
78
- result = self.get_mojo_name(full_name, found_error)
115
+ result = self.get_mojo_name(full_name)
116
+ if not result:
117
+ result = self.get_fuzz_name(full_name, found_error)
118
+
79
119
  if result:
80
120
  return self.get_number_first_last(result[0], result[1])
81
121
  return None
@@ -123,7 +163,7 @@ class Member(MojoSkel):
123
163
  self, full_name: str, found_error: bool = False
124
164
  ) -> Optional[tuple]:
125
165
  """
126
- Resolve a member name from a free-text full name.
166
+ Resolve a member name from a free-text full name
127
167
 
128
168
  **Search order**
129
169
 
@@ -132,14 +172,14 @@ class Member(MojoSkel):
132
172
  3. initial 1st letter + last
133
173
  4. initial 2nd letter + last (for two-letter initials)
134
174
 
135
- Returns (first_name, last_name) or None.
175
+ Returns (first_name, last_name) or None
136
176
 
137
- :param full_name: Full name of the member to find.
138
- :param found_error: (optional) Raise ValueError if not found.
177
+ :param full_name: Full name of the member to find
178
+ :param found_error: (optional) Raise ValueError if not found
139
179
 
140
- :return: Membermojo name if found, else None.
180
+ :return: Membermojo name if found, else None
141
181
 
142
- :raises ValueError: If not found and `found_error` is True.
182
+ :raises ValueError: If not found and `found_error` is True
143
183
  """
144
184
 
145
185
  parts = full_name.strip().split()
@@ -207,11 +247,11 @@ class Member(MojoSkel):
207
247
 
208
248
  def get_first_last_name(self, member_number: int) -> Optional[str]:
209
249
  """
210
- Get full name for a given member number.
250
+ Get full name for a given member number
211
251
 
212
- :param member_number: Member number to look up.
252
+ :param member_number: Member number to look up
213
253
 
214
- :return: Full name as tuple, or None if not found.
254
+ :return: Full name as tuple, or None if not found
215
255
  """
216
256
  sql = f"""
217
257
  SELECT "first_name", "last_name"
@@ -225,11 +265,11 @@ class Member(MojoSkel):
225
265
 
226
266
  def get_name(self, member_number: int) -> Optional[str]:
227
267
  """
228
- Get full name for a given member number.
268
+ Get full name for a given member number
229
269
 
230
- :param member_number: Member number to look up.
270
+ :param member_number: Member number to look up
231
271
 
232
- :return: Full name as "First Last", or None if not found.
272
+ :return: Full name as "First Last", or None if not found
233
273
  """
234
274
 
235
275
  result = self.get_first_last_name(member_number)
@@ -238,3 +278,39 @@ class Member(MojoSkel):
238
278
  first_name, last_name = result
239
279
  return f"{first_name} {last_name}"
240
280
  return None
281
+
282
+ def get_fuzz_name(self, name: str, found_error: bool = False):
283
+ """
284
+ Fuzzy search for members by name using partial matching
285
+ Searches across first_name and last_name fields
286
+
287
+ :param name: Free text name to search for (partial match)
288
+
289
+
290
+ :return: Tuple of (first_name, last_name) or None
291
+
292
+ :raises ValueError: If not found and `found_error` is True
293
+ """
294
+
295
+ name = name.strip().lower()
296
+
297
+ # Get all members
298
+ self.cursor.execute(
299
+ f'SELECT *, LOWER(first_name || " " || last_name) AS full FROM "{self.table_name}"'
300
+ )
301
+ rows = self.cursor.fetchall()
302
+
303
+ choices = [row["full"] for row in rows]
304
+ matches = get_close_matches(name, choices, n=1, cutoff=0.7)
305
+
306
+ if not matches:
307
+ if found_error:
308
+ raise ValueError(
309
+ f"❌ Cannot find {name} in member database with fuzzy match."
310
+ )
311
+ return None
312
+
313
+ match = matches[0]
314
+ # return the sqlite row for the best match
315
+ row = next(r for r in rows if r["full"] == match)
316
+ return (row["first_name"], row["last_name"])
@@ -1,8 +1,8 @@
1
1
  """
2
- Module to import and interact with Membermojo completed_payments.csv data in SQLite.
2
+ Module to import and interact with Membermojo completed_payments.csv data in SQLite
3
3
 
4
4
  Provides automatic column type inference, robust CSV importing, and
5
- helper methods for querying the database.
5
+ helper methods for querying the database
6
6
  """
7
7
 
8
8
  from .mojo_common import MojoSkel
@@ -10,14 +10,15 @@ from .mojo_common import MojoSkel
10
10
 
11
11
  class Transaction(MojoSkel):
12
12
  """
13
- Handles importing and querying completed payment data.
13
+ Handles importing and querying completed payment data
14
14
 
15
15
  Extends:
16
- MojoSkel: Base class with transaction database operations.
16
+ MojoSkel: Base class with transaction database operations
17
17
 
18
- :param payment_db_path: Path to the SQLite database.
19
- :param table_name: (optional) Name of the table. Defaults to "payments".
20
- :param db_key: (optional) key to unlock the encrypted sqlite database, unencrypted if unset.
18
+ :param payment_db_path: Path to the SQLite database
19
+ :param db_key: key to unlock the encrypted sqlite database,
20
+ unencrypted if sqlcipher3 not installed or unset
21
+ :param table_name: (optional) Name of the table. Defaults to "payments"
21
22
  """
22
23
 
23
24
  def __init__(
@@ -27,6 +28,6 @@ class Transaction(MojoSkel):
27
28
  table_name: str = "payments",
28
29
  ):
29
30
  """
30
- Initialize the Transaction object.
31
+ Initialize the Transaction object
31
32
  """
32
33
  super().__init__(payment_db_path, db_key, table_name)
@@ -0,0 +1,12 @@
1
+ """
2
+ Classes for use in sqlite row matching
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class Like:
10
+ """Marker type for SQL LIKE comparisons"""
11
+
12
+ pattern: str