memberjojo 2.2__py3-none-any.whl → 3.0__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,22 +1,25 @@
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
6
  from collections import defaultdict, Counter
7
7
  from csv import DictReader
8
8
  from dataclasses import dataclass
9
+ from io import StringIO
9
10
  from pathlib import Path
10
- from typing import Any, Tuple
11
+ from typing import Any, IO, Tuple
11
12
 
12
13
  import re
13
14
  import sqlite3 as sqlite3_builtin
14
15
 
16
+ import requests
17
+
15
18
 
16
19
  @dataclass(frozen=True)
17
20
  class DiffRow:
18
21
  """
19
- Represents a single diff result.
22
+ Represents a single diff result
20
23
 
21
24
  - diff_type: 'added' | 'deleted' | 'changed'
22
25
  - preview: tuple of values, with preview[0] == key
@@ -33,11 +36,11 @@ class DiffRow:
33
36
 
34
37
  def _normalize(name: str) -> str:
35
38
  """
36
- Normalize a column name: lowercase, remove symbols, convert to snake case.
39
+ Normalize a column name: lowercase, remove symbols, convert to snake case
37
40
 
38
- :param name: Raw name to normalize.
41
+ :param name: Raw name to normalize
39
42
 
40
- :return: Normalized lowercase string in snake case with no symbols.
43
+ :return: Normalized lowercase string in snake case with no symbols
41
44
  """
42
45
  name = name.strip().lower()
43
46
  name = re.sub(r"[^a-z0-9]+", "_", name)
@@ -46,7 +49,7 @@ def _normalize(name: str) -> str:
46
49
 
47
50
  def _guess_type(value: any) -> str:
48
51
  """
49
- 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'
50
53
 
51
54
  :param value: entry from sqlite database to guess the type of
52
55
 
@@ -71,8 +74,8 @@ def _guess_type(value: any) -> str:
71
74
 
72
75
  def infer_columns_from_rows(rows: list[dict]) -> dict[str, str]:
73
76
  """
74
- Infer column types from CSV rows.
75
- Returns mapping: normalized column name -> SQLite type.
77
+ Infer column types from CSV rows
78
+ Returns mapping: normalized column name -> SQLite type
76
79
 
77
80
  :param rows: list of rows to use for inference
78
81
 
@@ -104,75 +107,100 @@ def infer_columns_from_rows(rows: list[dict]) -> dict[str, str]:
104
107
 
105
108
  def _create_table_from_columns(table_name: str, columns: dict[str, str]) -> str:
106
109
  """
107
- 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
108
112
 
109
- :param table_name: Table to use when creating columns.
110
- :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
111
115
 
112
- :return: SQL commands to create the table.
116
+ :return: SQL commands to create the table
113
117
  """
114
- col_defs = []
115
- first = True
118
+ col_defs = ["rowid INTEGER PRIMARY KEY AUTOINCREMENT"]
116
119
 
117
120
  for col, col_type in columns.items():
118
- if first:
119
- col_defs.append(f'"{col}" {col_type} PRIMARY KEY')
120
- first = False
121
- else:
122
- col_defs.append(f'"{col}" {col_type}')
121
+ col_defs.append(f'"{col}" {col_type}')
123
122
 
124
123
  return (
125
124
  f'CREATE TABLE IF NOT EXISTS "{table_name}" (\n' + ",\n".join(col_defs) + "\n)"
126
125
  )
127
126
 
128
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
+
129
144
  # -----------------------
130
145
  # CSV Import
131
146
  # -----------------------
132
147
 
133
148
 
134
- def import_csv_helper(conn, table_name: str, csv_path: Path):
149
+ def import_data(conn, table_name: str, reader: DictReader, merge: bool = False):
135
150
  """
136
- Import CSV into database using given cursor.
137
- Column types inferred automatically.
151
+ Import data in the DictReader into the SQLite3 database at conn
138
152
 
139
- :param conn: SQLite database connection to use.
140
- :param table_name: Table to import the CSV into.
141
- :param csv_path: Path like path of the CSV file to import.
153
+ :param conn: SQLite database connection to use
154
+ :param table_name: Name of the table to import into
155
+ :param reader: A Dictreader object to import from
156
+ :param merge: (optional) If True, merge into existing table. Defaults to False.
142
157
  """
143
- if not csv_path.exists():
144
- raise FileNotFoundError(f"CSV file not found: {csv_path}")
145
158
 
146
- # Read CSV rows
147
- with csv_path.open(newline="", encoding="utf-8") as f:
148
- reader = list(DictReader(f))
149
- if not reader:
150
- raise ValueError("CSV file is empty.")
151
- inferred_cols = infer_columns_from_rows(reader)
152
-
153
- cursor = conn.cursor()
159
+ cursor = conn.cursor()
160
+ if not merge or not table_exists(cursor, table_name):
154
161
  # Drop existing table
155
162
  cursor.execute(f'DROP TABLE IF EXISTS "{table_name}";')
156
-
163
+ inferred_cols = infer_columns_from_rows(reader)
157
164
  # Create table
158
165
  create_sql = _create_table_from_columns(table_name, inferred_cols)
159
166
  cursor.execute(create_sql)
160
167
 
161
- # Insert rows
162
- cols = list(reader[0].keys())
163
- norm_map = {c: _normalize(c) for c in cols}
164
- colnames = ",".join(f'"{norm_map[c]}"' for c in cols)
165
- placeholders = ",".join("?" for _ in cols)
166
- insert_sql = f'INSERT INTO "{table_name}" ({colnames}) VALUES ({placeholders})'
168
+ # Insert rows
169
+ cols = list(reader[0].keys())
170
+ norm_map = {c: _normalize(c) for c in cols}
171
+ colnames = ",".join(f'"{norm_map[c]}"' for c in cols)
172
+ placeholders = ",".join("?" for _ in cols)
173
+ insert_sql = f'INSERT INTO "{table_name}" ({colnames}) VALUES ({placeholders})'
167
174
 
168
- for row in reader:
169
- values = [row[c] if row[c] != "" else None for c in cols]
170
- cursor.execute(insert_sql, values)
175
+ for row in reader:
176
+ values = [row[c] if row[c] != "" else None for c in cols]
177
+ cursor.execute(insert_sql, values)
171
178
 
172
179
  cursor.close()
173
180
  conn.commit()
174
181
 
175
182
 
183
+ def import_csv_helper(conn, table_name: str, csv_path: Path, merge: bool = False):
184
+ """
185
+ Import CSV into database using given cursor
186
+ Column types inferred automatically
187
+
188
+ :param conn: SQLite database connection to use
189
+ :param table_name: Table to import the CSV into
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.
192
+ """
193
+ if not csv_path.exists():
194
+ raise FileNotFoundError(f"CSV file not found: {csv_path}")
195
+
196
+ # Read CSV rows
197
+ with csv_path.open(newline="", encoding="utf-8") as f:
198
+ reader = list(DictReader(f))
199
+ if not reader:
200
+ raise ValueError("CSV file is empty.")
201
+ import_data(conn, table_name, reader, merge=merge)
202
+
203
+
176
204
  # -----------------------
177
205
  # diff generation
178
206
  # -----------------------
@@ -180,7 +208,7 @@ def import_csv_helper(conn, table_name: str, csv_path: Path):
180
208
 
181
209
  def _diffrow_from_sql_row(row: sqlite3_builtin.Row) -> DiffRow:
182
210
  """
183
- Convert a sqlite3.Row from generate_sql_diff into DiffRow.
211
+ Convert a sqlite3.Row from generate_sql_diff into DiffRow
184
212
  Row shape:
185
213
  (diff_type, col1, col2, col3, ...)
186
214
 
@@ -195,7 +223,7 @@ def _diffrow_from_sql_row(row: sqlite3_builtin.Row) -> DiffRow:
195
223
 
196
224
 
197
225
  def diff_cipher_tables(
198
- cipher_conn,
226
+ conn,
199
227
  *,
200
228
  new_table: str,
201
229
  old_table: str,
@@ -204,7 +232,7 @@ def diff_cipher_tables(
204
232
  Copy old and new tables from SQLCipher into a single
205
233
  in-memory sqlite3 database and diff them there.
206
234
 
207
- :param cipher_conn: sqlite connection to the encrypted db
235
+ :param conn: sqlite connection to the db
208
236
  :param new_table: name of the new table for comparison
209
237
  :param old_table: name of the old table for comparison
210
238
 
@@ -216,8 +244,8 @@ def diff_cipher_tables(
216
244
 
217
245
  try:
218
246
  for table in (old_table, new_table):
219
- # 1. Clone schema using SQLite itself
220
- schema_sql = cipher_conn.execute(
247
+ # Clone schema using SQLite itself
248
+ schema_sql = conn.execute(
221
249
  """
222
250
  SELECT sql
223
251
  FROM sqlite_master
@@ -232,7 +260,7 @@ def diff_cipher_tables(
232
260
  plain.execute(schema_sql[0])
233
261
 
234
262
  # 2. Copy data
235
- rows = cipher_conn.execute(f"SELECT * FROM {table}")
263
+ rows = conn.execute(f"SELECT * FROM {table}")
236
264
  cols = [d[0] for d in rows.description]
237
265
 
238
266
  col_list = ", ".join(cols)
@@ -263,9 +291,9 @@ def _generate_sql_diff(
263
291
  old_table: str,
264
292
  ) -> list[sqlite3_builtin.Row]:
265
293
  """
266
- Generate a diff between two tables using standard SQLite features.
294
+ Generate a diff between two tables using standard SQLite features
267
295
 
268
- - The FIRST column is the primary key.
296
+ - Uses rowid as the primary key for joining
269
297
  - Returned row shape:
270
298
  (diff_type, preview_col1, preview_col2, preview_col3, ...)
271
299
 
@@ -276,7 +304,7 @@ def _generate_sql_diff(
276
304
  :return: list of sqlite rows that are changed
277
305
  """
278
306
 
279
- # 1. Introspect schema (order-preserving)
307
+ # Introspect schema (order-preserving)
280
308
  cols_info = conn.execute(f"PRAGMA table_info({new_table})").fetchall()
281
309
 
282
310
  if not cols_info:
@@ -284,16 +312,20 @@ def _generate_sql_diff(
284
312
 
285
313
  cols = [row[1] for row in cols_info]
286
314
 
287
- key = cols[0]
288
- non_key_cols = cols[1:]
315
+ # Exclude rowid from consideration for key or data
316
+ main_cols = [c for c in cols if c != "rowid"]
289
317
 
290
- # 2. Preview columns (key first, limit for readability)
318
+ # First column is key, others are for comparison
319
+ key = main_cols[0]
320
+ non_key_cols = main_cols[1:]
321
+
322
+ # Preview columns (key first, then others for readability)
291
323
  preview_cols = [key] + non_key_cols[:5]
292
324
 
293
325
  new_preview = ", ".join(f"n.{c}" for c in preview_cols)
294
326
  old_preview = ", ".join(f"o.{c}" for c in preview_cols)
295
327
 
296
- # 3. Row-value comparison (NULL-safe)
328
+ # Row-value comparison (NULL-safe)
297
329
  if non_key_cols:
298
330
  changed_predicate = (
299
331
  f"({', '.join(f'n.{c}' for c in non_key_cols)}) "
@@ -333,3 +365,39 @@ def _generate_sql_diff(
333
365
  """
334
366
 
335
367
  return list(conn.execute(sql))
368
+
369
+
370
+ def download_csv_helper(
371
+ conn, table_name: str, url: str, session: requests.Session, merge: bool = False
372
+ ) -> IO[str]:
373
+ """
374
+ Download url into a StringIO file object using streaming
375
+ and import into database
376
+
377
+ :param conn: The SQLite3 database connection to use
378
+ :param table_name: The name of the table to import it into
379
+ :param url: URL of the csv to download
380
+ :param session: A requests session to use for the download
381
+ :param merge: (optional) If True, merge into existing table. Defaults to False.
382
+ """
383
+
384
+ print(f"☁️ Downloading from: {url}")
385
+
386
+ # Enable streaming
387
+ with session.get(url, stream=True) as resp:
388
+ resp.raise_for_status()
389
+
390
+ # Initialize the string buffer
391
+ string_buffer = StringIO()
392
+
393
+ # Stream decoded text
394
+ # decode_unicode=True uses the encoding from the response headers
395
+ for chunk in resp.iter_content(chunk_size=8192, decode_unicode=True):
396
+ if chunk:
397
+ string_buffer.write(chunk)
398
+
399
+ # Reset pointer to the beginning for DictReader
400
+ string_buffer.seek(0)
401
+
402
+ print(f"✅ Downloaded with encoding {resp.encoding}.")
403
+ import_data(conn, table_name, list(DictReader(string_buffer)), merge=merge)
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,63 @@ 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
+
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
+
36
79
  def get_number_first_last(
37
80
  self, first_name: str, last_name: str, found_error: bool = False
38
81
  ) -> Optional[int]:
39
82
  """
40
- Find a member number based on first and last name (case-insensitive).
83
+ Find a member number based on first and last name (case-insensitive)
41
84
 
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.
85
+ :param first_name: First name of the member
86
+ :param last_name: Last name of the member
87
+ :param found_error: (optional): If True, raises ValueError if not found
45
88
 
46
- :return: The member number if found, otherwise None.
89
+ :return: The member number if found, otherwise None
47
90
 
48
- :raises ValueError: If not found and `found_error` is True.
91
+ :raises ValueError: If not found and `found_error` is True
49
92
  """
50
93
  sql = f"""
51
94
  SELECT "member_number"
@@ -54,6 +97,12 @@ class Member(MojoSkel):
54
97
  """
55
98
  self.cursor.execute(sql, (first_name, last_name))
56
99
  result = self.cursor.fetchone()
100
+ if self.debug:
101
+ print("Sql:")
102
+ pprint(sql)
103
+ print(f"First Name: {first_name} Last Name: {last_name}")
104
+ print("Result:")
105
+ pprint(result[0])
57
106
 
58
107
  if not result and found_error:
59
108
  raise ValueError(
@@ -64,18 +113,22 @@ class Member(MojoSkel):
64
113
 
65
114
  def get_number(self, full_name: str, found_error: bool = False) -> Optional[int]:
66
115
  """
67
- Find a member number by passed full_name.
116
+ Find a member number by passed full_name
68
117
  Tries first and last, and then middle last if 3 words,
69
- Then initial of first name if initials passed.
118
+ Then initial of first name if initials passed
119
+ Finnaly a fuzzy lookup is tried
70
120
 
71
- :param full_name: Full name of the member.
72
- :param found_error: (optional) Raise ValueError if not found.
121
+ :param full_name: Full name of the member
122
+ :param found_error: (optional) Raise ValueError if not found
73
123
 
74
- :return: Member number if found, else None.
124
+ :return: Member number if found, else None
75
125
 
76
- :raises ValueError: If not found and `found_error` is True.
126
+ :raises ValueError: If not found and `found_error` is True
77
127
  """
78
- result = self.get_mojo_name(full_name, found_error)
128
+ result = self.get_mojo_name(full_name)
129
+ if not result:
130
+ result = self.get_fuzz_name(full_name, found_error)
131
+
79
132
  if result:
80
133
  return self.get_number_first_last(result[0], result[1])
81
134
  return None
@@ -123,7 +176,7 @@ class Member(MojoSkel):
123
176
  self, full_name: str, found_error: bool = False
124
177
  ) -> Optional[tuple]:
125
178
  """
126
- Resolve a member name from a free-text full name.
179
+ Resolve a member name from a free-text full name
127
180
 
128
181
  **Search order**
129
182
 
@@ -132,14 +185,14 @@ class Member(MojoSkel):
132
185
  3. initial 1st letter + last
133
186
  4. initial 2nd letter + last (for two-letter initials)
134
187
 
135
- Returns (first_name, last_name) or None.
188
+ Returns (first_name, last_name) or None
136
189
 
137
- :param full_name: Full name of the member to find.
138
- :param found_error: (optional) Raise ValueError if not found.
190
+ :param full_name: Full name of the member to find
191
+ :param found_error: (optional) Raise ValueError if not found
139
192
 
140
- :return: Membermojo name if found, else None.
193
+ :return: Membermojo name if found, else None
141
194
 
142
- :raises ValueError: If not found and `found_error` is True.
195
+ :raises ValueError: If not found and `found_error` is True
143
196
  """
144
197
 
145
198
  parts = full_name.strip().split()
@@ -207,11 +260,11 @@ class Member(MojoSkel):
207
260
 
208
261
  def get_first_last_name(self, member_number: int) -> Optional[str]:
209
262
  """
210
- Get full name for a given member number.
263
+ Get full name for a given member number
211
264
 
212
- :param member_number: Member number to look up.
265
+ :param member_number: Member number to look up
213
266
 
214
- :return: Full name as tuple, or None if not found.
267
+ :return: Full name as tuple, or None if not found
215
268
  """
216
269
  sql = f"""
217
270
  SELECT "first_name", "last_name"
@@ -225,11 +278,11 @@ class Member(MojoSkel):
225
278
 
226
279
  def get_name(self, member_number: int) -> Optional[str]:
227
280
  """
228
- Get full name for a given member number.
281
+ Get full name for a given member number
229
282
 
230
- :param member_number: Member number to look up.
283
+ :param member_number: Member number to look up
231
284
 
232
- :return: Full name as "First Last", or None if not found.
285
+ :return: Full name as "First Last", or None if not found
233
286
  """
234
287
 
235
288
  result = self.get_first_last_name(member_number)
@@ -238,3 +291,39 @@ class Member(MojoSkel):
238
291
  first_name, last_name = result
239
292
  return f"{first_name} {last_name}"
240
293
  return None
294
+
295
+ def get_fuzz_name(self, name: str, found_error: bool = False):
296
+ """
297
+ Fuzzy search for members by name using partial matching
298
+ Searches across first_name and last_name fields
299
+
300
+ :param name: Free text name to search for (partial match)
301
+
302
+
303
+ :return: Tuple of (first_name, last_name) or None
304
+
305
+ :raises ValueError: If not found and `found_error` is True
306
+ """
307
+
308
+ name = name.strip().lower()
309
+
310
+ # Get all members
311
+ self.cursor.execute(
312
+ f'SELECT *, LOWER(first_name || " " || last_name) AS full FROM "{self.table_name}"'
313
+ )
314
+ rows = self.cursor.fetchall()
315
+
316
+ choices = [row["full"] for row in rows]
317
+ matches = get_close_matches(name, choices, n=1, cutoff=0.7)
318
+
319
+ if not matches:
320
+ if found_error:
321
+ raise ValueError(
322
+ f"❌ Cannot find {name} in member database with fuzzy match."
323
+ )
324
+ return None
325
+
326
+ match = matches[0]
327
+ # return the sqlite row for the best match
328
+ row = next(r for r in rows if r["full"] == match)
329
+ 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