GameSentenceMiner 2.18.2__py3-none-any.whl → 2.18.4__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.

Potentially problematic release.


This version of GameSentenceMiner might be problematic. Click here for more details.

@@ -93,6 +93,7 @@ class SQLiteDBTable:
93
93
  _types: List[type] = []
94
94
  _pk: str = 'id'
95
95
  _auto_increment: bool = True
96
+ _column_order_cache: Optional[List[str]] = None # Cache for actual column order
96
97
 
97
98
  def __init_subclass__(cls, **kwargs):
98
99
  super().__init_subclass__(**kwargs)
@@ -104,6 +105,7 @@ class SQLiteDBTable:
104
105
  @classmethod
105
106
  def set_db(cls, db: SQLiteDB):
106
107
  cls._db = db
108
+ cls._column_order_cache = None # Reset cache when database changes
107
109
  # Ensure table exists
108
110
  if not db.table_exists(cls._table):
109
111
  fields_def = ', '.join([f"{field} TEXT" for field in cls._fields])
@@ -115,6 +117,7 @@ class SQLiteDBTable:
115
117
  for field in cls._fields:
116
118
  if field not in existing_columns:
117
119
  db.execute(f"ALTER TABLE {cls._table} ADD COLUMN {field} TEXT", commit=True)
120
+ cls._column_order_cache = None # Reset cache when schema changes
118
121
 
119
122
  @classmethod
120
123
  def all(cls: Type[T]) -> List[T]:
@@ -137,49 +140,80 @@ class SQLiteDBTable:
137
140
  if not row:
138
141
  return None
139
142
  obj = cls()
140
- fields = [cls._pk] + cls._fields
141
- for i, field in enumerate(fields):
142
- if i == 0 and field == cls._pk:
143
- if cls._types[i] is int:
144
- setattr(obj, field, int(row[i])
145
- if row[i] is not None else None)
146
- elif cls._types[i] is str:
147
- setattr(obj, field, str(row[i])
148
- if row[i] is not None else None)
149
- continue
150
- if cls._types[i] is str:
151
- if not row[i]:
152
- setattr(obj, field, "")
153
- elif (row[i].startswith('[') or row[i].startswith('{')):
154
- try:
155
- setattr(obj, field, json.loads(row[i]))
156
- except json.JSONDecodeError:
157
- setattr(obj, field, row[i])
158
- else:
159
- setattr(obj, field, str(row[i])
160
- if row[i] is not None else None)
161
- elif cls._types[i] is list:
162
- try:
163
- setattr(obj, field, json.loads(row[i]) if row[i] else [])
164
- except json.JSONDecodeError:
165
- setattr(obj, field, [])
166
- elif cls._types[i] is int:
167
- setattr(obj, field, int(row[i])
168
- if row[i] is not None else None)
169
- elif cls._types[i] is float:
170
- setattr(obj, field, float(row[i])
171
- if row[i] is not None else None)
172
- elif cls._types[i] is bool:
173
- setattr(obj, field, bool(row[i])
174
- if row[i] is not None else None)
175
- elif cls._types[i] is dict:
143
+
144
+ try:
145
+ # Get actual column order from database schema
146
+ actual_columns = cls.get_actual_column_order()
147
+ expected_fields = [cls._pk] + cls._fields
148
+
149
+ # Create a mapping from actual column positions to expected field positions
150
+ column_mapping = {}
151
+ for i, actual_col in enumerate(actual_columns):
152
+ if actual_col in expected_fields:
153
+ expected_index = expected_fields.index(actual_col)
154
+ column_mapping[i] = expected_index
155
+
156
+ # Process each column in the row based on the mapping
157
+ for actual_pos, row_value in enumerate(row):
158
+ if actual_pos not in column_mapping:
159
+ continue # Skip unknown columns
160
+
161
+ expected_pos = column_mapping[actual_pos]
162
+ field = expected_fields[expected_pos]
163
+ field_type = cls._types[expected_pos]
164
+
165
+ cls._set_field_value(obj, field, field_type, row_value, expected_pos == 0 and field == cls._pk)
166
+
167
+ except Exception as e:
168
+ # Fallback to original behavior if schema-based mapping fails
169
+ logger.warning(f"Column mapping failed for {cls._table}, falling back to positional mapping: {e}")
170
+ expected_fields = [cls._pk] + cls._fields
171
+ for i, field in enumerate(expected_fields):
172
+ if i >= len(row):
173
+ break # Safety check
174
+ field_type = cls._types[i]
175
+ cls._set_field_value(obj, field, field_type, row[i], i == 0 and field == cls._pk)
176
+
177
+ return obj
178
+
179
+ @classmethod
180
+ def _set_field_value(cls, obj, field: str, field_type: type, row_value, is_pk: bool = False):
181
+ """Helper method to set field value with proper type conversion."""
182
+ if is_pk:
183
+ if field_type is int:
184
+ setattr(obj, field, int(row_value) if row_value is not None else None)
185
+ elif field_type is str:
186
+ setattr(obj, field, str(row_value) if row_value is not None else None)
187
+ return
188
+
189
+ if field_type is str:
190
+ if not row_value:
191
+ setattr(obj, field, "")
192
+ elif isinstance(row_value, str) and (row_value.startswith('[') or row_value.startswith('{')):
176
193
  try:
177
- setattr(obj, field, json.loads(row[i]) if row[i] else {})
194
+ setattr(obj, field, json.loads(row_value))
178
195
  except json.JSONDecodeError:
179
- setattr(obj, field, {})
196
+ setattr(obj, field, row_value)
180
197
  else:
181
- setattr(obj, field, row[i])
182
- return obj
198
+ setattr(obj, field, str(row_value) if row_value is not None else None)
199
+ elif field_type is list:
200
+ try:
201
+ setattr(obj, field, json.loads(row_value) if row_value else [])
202
+ except json.JSONDecodeError:
203
+ setattr(obj, field, [])
204
+ elif field_type is int:
205
+ setattr(obj, field, int(row_value) if row_value is not None else None)
206
+ elif field_type is float:
207
+ setattr(obj, field, float(row_value) if row_value is not None else None)
208
+ elif field_type is bool:
209
+ setattr(obj, field, bool(row_value) if row_value is not None else None)
210
+ elif field_type is dict:
211
+ try:
212
+ setattr(obj, field, json.loads(row_value) if row_value else {})
213
+ except json.JSONDecodeError:
214
+ setattr(obj, field, {})
215
+ else:
216
+ setattr(obj, field, row_value)
183
217
 
184
218
  def save(self, retry=1):
185
219
  try:
@@ -245,9 +279,9 @@ class SQLiteDBTable:
245
279
 
246
280
  def add_column(self, column_name: str, new_column_type: str = "TEXT"):
247
281
  try:
248
- index = self._fields.index(column_name) + 1
249
282
  self._db.execute(
250
283
  f"ALTER TABLE {self._table} ADD COLUMN {column_name} {new_column_type}", commit=True)
284
+ self.__class__._column_order_cache = None # Reset cache when schema changes
251
285
  logger.info(f"Added column {column_name} to {self._table}")
252
286
  except sqlite3.OperationalError as e:
253
287
  if "duplicate column name" in str(e):
@@ -286,11 +320,13 @@ class SQLiteDBTable:
286
320
  def rename_column(cls, old_column: str, new_column: str):
287
321
  cls._db.execute(
288
322
  f"ALTER TABLE {cls._table} RENAME COLUMN {old_column} TO {new_column}", commit=True)
323
+ cls._column_order_cache = None # Reset cache when schema changes
289
324
 
290
325
  @classmethod
291
326
  def drop_column(cls, column_name: str):
292
327
  cls._db.execute(
293
328
  f"ALTER TABLE {cls._table} DROP COLUMN {column_name}", commit=True)
329
+ cls._column_order_cache = None # Reset cache when schema changes
294
330
 
295
331
  @classmethod
296
332
  def get_column_type(cls, column_name: str) -> Optional[str]:
@@ -315,6 +351,36 @@ class SQLiteDBTable:
315
351
  f"UPDATE {cls._table} SET {new_column} = CAST({old_column} AS {new_type})", commit=True)
316
352
  cls._db.execute(
317
353
  f"ALTER TABLE {cls._table} DROP COLUMN {old_column}", commit=True)
354
+ cls._column_order_cache = None # Reset cache when schema changes
355
+
356
+ @classmethod
357
+ def get_actual_column_order(cls) -> List[str]:
358
+ """Get the actual column order from the database schema."""
359
+ if cls._column_order_cache is not None:
360
+ return cls._column_order_cache
361
+
362
+ # Use direct database access to avoid recursion through from_row()
363
+ with cls._db._lock:
364
+ import sqlite3
365
+ with sqlite3.connect(cls._db.db_path, check_same_thread=False) as conn:
366
+ cursor = conn.cursor()
367
+ cursor.execute(f"PRAGMA table_info({cls._table})")
368
+ columns_info = cursor.fetchall()
369
+
370
+ # Each row is (cid, name, type, notnull, dflt_value, pk)
371
+ # Sort by column id (cid) to get the actual order
372
+ sorted_columns = sorted(columns_info, key=lambda x: x[0])
373
+ column_order = [col[1] for col in sorted_columns]
374
+
375
+ # Cache the result
376
+ cls._column_order_cache = column_order
377
+ return column_order
378
+
379
+ @classmethod
380
+ def get_expected_column_list(cls) -> str:
381
+ """Get comma-separated list of columns in expected order for explicit SELECT queries."""
382
+ expected_fields = [cls._pk] + cls._fields
383
+ return ', '.join(expected_fields)
318
384
 
319
385
 
320
386
  class AIModelsTable(SQLiteDBTable):
@@ -188,41 +188,41 @@ class OverlayProcessor:
188
188
  For primary monitor, excludes taskbar. For others, returns full monitor area.
189
189
  monitor_index: 0 = primary monitor, 1+ = others (as in mss.monitors).
190
190
  """
191
- set_dpi_awareness()
191
+ # set_dpi_awareness()
192
192
  with mss.mss() as sct:
193
193
  monitors = sct.monitors[1:]
194
- return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
195
- # if is_windows() and monitor_index == 0:
196
- # from ctypes import wintypes
197
- # import ctypes
198
- # # Get work area for primary monitor (ignores taskbar)
199
- # SPI_GETWORKAREA = 0x0030
200
- # rect = wintypes.RECT()
201
- # res = ctypes.windll.user32.SystemParametersInfoW(
202
- # SPI_GETWORKAREA, 0, ctypes.byref(rect), 0
203
- # )
204
- # if not res:
205
- # raise ctypes.WinError()
194
+ # return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
195
+ if is_windows() and monitor_index == 0:
196
+ from ctypes import wintypes
197
+ import ctypes
198
+ # Get work area for primary monitor (ignores taskbar)
199
+ SPI_GETWORKAREA = 0x0030
200
+ rect = wintypes.RECT()
201
+ res = ctypes.windll.user32.SystemParametersInfoW(
202
+ SPI_GETWORKAREA, 0, ctypes.byref(rect), 0
203
+ )
204
+ if not res:
205
+ raise ctypes.WinError()
206
206
 
207
- # return {
208
- # "left": rect.left,
209
- # "top": rect.top,
210
- # "width": rect.right - rect.left,
211
- # "height": rect.bottom - rect.top,
212
- # }
213
- # elif is_windows() and monitor_index > 0:
214
- # # Secondary monitors: just return with a guess of how tall the taskbar is
215
- # taskbar_height_guess = 48 # A common taskbar height, may vary
216
- # mon = monitors[monitor_index]
217
- # return {
218
- # "left": mon["left"],
219
- # "top": mon["top"],
220
- # "width": mon["width"],
221
- # "height": mon["height"] - taskbar_height_guess
222
- # }
223
- # else:
224
- # # For non-Windows systems or unspecified monitors, return the monitor area as-is
225
- # return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
207
+ return {
208
+ "left": rect.left,
209
+ "top": rect.top,
210
+ "width": rect.right - rect.left,
211
+ "height": rect.bottom - rect.top,
212
+ }
213
+ elif is_windows() and monitor_index > 0:
214
+ # Secondary monitors: just return with a guess of how tall the taskbar is
215
+ taskbar_height_guess = 48 # A common taskbar height, may vary
216
+ mon = monitors[monitor_index]
217
+ return {
218
+ "left": mon["left"],
219
+ "top": mon["top"],
220
+ "width": mon["width"],
221
+ "height": mon["height"] - taskbar_height_guess
222
+ }
223
+ else:
224
+ # For non-Windows systems or unspecified monitors, return the monitor area as-is
225
+ return monitors[monitor_index] if 0 <= monitor_index < len(monitors) else monitors[0]
226
226
 
227
227
 
228
228
  def _get_full_screenshot(self) -> Tuple[Image.Image | None, int, int]:
@@ -1182,7 +1182,7 @@ def register_database_api_routes(app):
1182
1182
  })
1183
1183
 
1184
1184
  except Exception as e:
1185
- logger.error(f"Unexpected error in api_stats: {e}")
1185
+ logger.error(f"Unexpected error in api_stats: {e}", exc_info=True)
1186
1186
  return jsonify({'error': 'Failed to generate statistics'}), 500
1187
1187
 
1188
1188
  @app.route('/api/goals-today', methods=['GET'])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GameSentenceMiner
3
- Version: 2.18.2
3
+ Version: 2.18.4
4
4
  Summary: A tool for mining sentences from games. Update: Overlay?
5
5
  Author-email: Beangate <bpwhelan95@gmail.com>
6
6
  License: MIT License
@@ -41,10 +41,10 @@ GameSentenceMiner/ui/config_gui.py,sha256=Nt6uuJrinOrSpDdrw92zdd1GvyQNjOoGvkZQdG
41
41
  GameSentenceMiner/ui/screenshot_selector.py,sha256=AKML87MpgYQeSuj1F10GngpNrn9qp06zLLzNRwrQWM8,8900
42
42
  GameSentenceMiner/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  GameSentenceMiner/util/configuration.py,sha256=fYlU4Qrr6T-k_G2MY2rgoQ9EV_k7XBfbtsO8q7PKTsU,46653
44
- GameSentenceMiner/util/db.py,sha256=3Fs75pGJyf8-3y3k6Zfpgp8z9tYL9-ZvQUk_FgDc9Vw,28664
44
+ GameSentenceMiner/util/db.py,sha256=1DjGjlwWnPefmQfzvMqqFPW0a0qeO-fIXE1YqKiok18,32000
45
45
  GameSentenceMiner/util/electron_config.py,sha256=KfeJToeFFVw0IR5MKa-gBzpzaGrU-lyJbR9z-sDEHYU,8767
46
46
  GameSentenceMiner/util/ffmpeg.py,sha256=cAzztfY36Xf2WvsJDjavoiMOvA9ac2GVdCrSB4LzHk4,29007
47
- GameSentenceMiner/util/get_overlay_coords.py,sha256=iAA49bF_8jJXNR6bEGEVAuXsUwYMW5tcfnCGiLyAzso,22172
47
+ GameSentenceMiner/util/get_overlay_coords.py,sha256=c37zVKPxFqwIi5XjZKjxRCW_Y8W4e1MX7vSLLwcOhs4,22116
48
48
  GameSentenceMiner/util/gsm_utils.py,sha256=mASECTmN10c2yPL4NEfLg0Y0YWwFso1i6r_hhJPR3MY,10974
49
49
  GameSentenceMiner/util/model.py,sha256=R-_RYTYLSDNgBoVTPuPBcIHeOznIqi_vBzQ7VQ20WYk,6727
50
50
  GameSentenceMiner/util/notification.py,sha256=YBhf_mSo_i3cjBz-pmeTPx3wchKiG9BK2VBdZSa2prQ,4597
@@ -59,7 +59,7 @@ GameSentenceMiner/util/downloader/oneocr_dl.py,sha256=l3s9Z-x1b57GX048o5h-MVv0UT
59
59
  GameSentenceMiner/util/win10toast/__init__.py,sha256=6TL2w6rzNmpJEp6_v2cAJP_7ExA3UsKzwdM08pNcVfE,5341
60
60
  GameSentenceMiner/util/win10toast/__main__.py,sha256=5MYnBcFj8y_6Dyc1kiPd0_FsUuh4yl1cv5wsleU6V4w,668
61
61
  GameSentenceMiner/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
- GameSentenceMiner/web/database_api.py,sha256=pY1j8b1H5VdkDv5aaZjBnpx4tSNfJhfsuukyty6jbi0,84812
62
+ GameSentenceMiner/web/database_api.py,sha256=Ph30uGAJFSxRkBdrZglXKsuwuKP46RxjzGODMO5aaLc,84827
63
63
  GameSentenceMiner/web/events.py,sha256=6Vyz5c9MdpMIa7Zqljqhap2XFQnAVYJ0CdQV64TSZsA,5119
64
64
  GameSentenceMiner/web/gsm_websocket.py,sha256=B0VKpxmsRu0WRh5nFWlpDPBQ6-K2ed7TEIa0O6YWeoo,4166
65
65
  GameSentenceMiner/web/service.py,sha256=YZchmScTn7AX_GkwV1ULEK6qjdOnJcpc3qfMwDf7cUE,5363
@@ -124,9 +124,9 @@ GameSentenceMiner/web/templates/components/kanji_grid/thousand_character_classic
124
124
  GameSentenceMiner/web/templates/components/kanji_grid/wanikani_levels.json,sha256=8wjnnaYQqmho6t5tMxrIAc03512A2tYhQh5dfsQnfAM,11372
125
125
  GameSentenceMiner/web/templates/components/kanji_grid/words_hk_frequency_list.json,sha256=wRkqZNPzz6DT9OTPHpXwfqW96Qb96stCQNNgOL-ZdKk,17535
126
126
  GameSentenceMiner/wip/__init___.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
- gamesentenceminer-2.18.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
128
- gamesentenceminer-2.18.2.dist-info/METADATA,sha256=ur53IngoSZHHs1vyMxSPuwZJhbkyBJajZ9exWCn8zQY,7487
129
- gamesentenceminer-2.18.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
130
- gamesentenceminer-2.18.2.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
131
- gamesentenceminer-2.18.2.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
132
- gamesentenceminer-2.18.2.dist-info/RECORD,,
127
+ gamesentenceminer-2.18.4.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
128
+ gamesentenceminer-2.18.4.dist-info/METADATA,sha256=SXw7P4busY00L4EwV-Avnt2C29HYJGLoePm5WBEgIo8,7487
129
+ gamesentenceminer-2.18.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
130
+ gamesentenceminer-2.18.4.dist-info/entry_points.txt,sha256=2APEP25DbfjSxGeHtwBstMH8mulVhLkqF_b9bqzU6vQ,65
131
+ gamesentenceminer-2.18.4.dist-info/top_level.txt,sha256=V1hUY6xVSyUEohb0uDoN4UIE6rUZ_JYx8yMyPGX4PgQ,18
132
+ gamesentenceminer-2.18.4.dist-info/RECORD,,