]> git.mxchange.org Git - fba.git/blob - fba/models/instances.py
Continued:
[fba.git] / fba / models / instances.py
1 # Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
2 # Copyright (C) 2023 Free Software Foundation
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published
6 # by the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17 import logging
18 import json
19 import time
20
21 import requests
22 import validators
23
24 from fba import database
25 from fba import utils
26
27 from fba.helpers import blacklist
28 from fba.helpers import cache
29 from fba.helpers import config
30 from fba.helpers import domain as domain_helper
31 from fba.helpers import tidyup
32
33 from fba.http import federation
34 from fba.http import network
35
36 from fba.models import error_log
37
38 logging.basicConfig(level=logging.INFO)
39 logger = logging.getLogger(__name__)
40 #logger.setLevel(logging.DEBUG)
41
42 # Found info from node, such as nodeinfo URL, detection mode that needs to be
43 # written to database. Both arrays must be filled at the same time or else
44 # update() will fail
45 _pending = {
46     # Detection mode
47     # NULL means all detection methods have failed (maybe still reachable instance)
48     "detection_mode"     : {},
49     # Found nodeinfo URL
50     "nodeinfo_url"       : {},
51     # Found total peers
52     "total_peers"        : {},
53     # Found total blocks
54     "total_blocks"       : {},
55     # Obfuscated domains
56     "obfuscated_blocks"  : {},
57     # Last fetched instances
58     "last_instance_fetch": {},
59     # Last updated
60     "last_updated"       : {},
61     # Last blocked
62     "last_blocked"       : {},
63     # Last nodeinfo (fetched)
64     "last_nodeinfo"      : {},
65     # Last response time
66     "last_response_time" : {},
67     # Last status code
68     "last_status_code"   : {},
69     # Last error details
70     "last_error_details" : {},
71     # Wether obfuscation has been used
72     "has_obfuscation"    : {},
73     # Original software
74     "original_software"  : {},
75     # Aliased software
76     "software"           : {},
77 }
78
79 def _set_data(key: str, domain: str, value: any):
80     logger.debug("key='%s',domain='%s',value[]='%s' - CALLED!", key, domain, type(value))
81     domain_helper.raise_on(domain)
82
83     if not isinstance(key, str):
84         raise ValueError(f"Parameter key[]='{type(key)}' is not of type 'str'")
85     elif key == "":
86         raise ValueError("Parameter 'key' is empty")
87     elif not key in _pending:
88         raise ValueError(f"key='{key}' not found in _pending")
89     elif blacklist.is_blacklisted(domain):
90         raise Exception(f"domain='{domain}' is blacklisted but function has been invoked")
91     elif not utils.is_primitive(value):
92         raise ValueError(f"value[]='{type(value)}' is not a primitive type")
93
94     # Set it
95     _pending[key][domain] = value
96
97     logger.debug("EXIT!")
98
99 def has_pending(domain: str) -> bool:
100     logger.debug("domain='%s' - CALLED!", domain)
101     domain_helper.raise_on(domain)
102
103     if not is_registered(domain):
104         raise ValueError(f"domain='{domain}' is not registered but function was invoked.")
105     elif blacklist.is_blacklisted(domain):
106         raise Exception(f"domain='{domain}' is blacklisted but function has been invoked")
107
108     has = False
109     logger.debug("Checking %d _pending array elements ...", len(_pending))
110     for key in _pending:
111         logger.debug("domain='%s',_pending[%s]()=%d", domain, key, len(_pending[key]))
112         if domain in _pending[key]:
113             logger.debug("domain='%s' at key='%s' has pending data ...", domain, key)
114             has = True
115             break
116
117     logger.debug("has='%s' - EXIT!", has)
118     return has
119
120 def update(domain: str):
121     logger.debug("domain='%s' - CALLED!", domain)
122     domain_helper.raise_on(domain)
123
124     if not is_registered(domain):
125         raise Exception(f"domain='{domain}' cannot be updated while not being registered")
126     elif not has_pending(domain):
127         raise Exception(f"domain='{domain}' has no pending instance data, but function invoked")
128     elif blacklist.is_blacklisted(domain):
129         raise Exception(f"domain='{domain}' is blacklisted but function has been invoked")
130
131     sql_string = ""
132     fields = list()
133
134     logger.debug("Checking %d _pending array elements ...", len(_pending))
135     for key in _pending:
136         logger.debug("Checking key='%s',domain='%s'", key, domain)
137         if domain in _pending[key]:
138             logger.debug("Adding '%s' for key='%s' ...", _pending[key][domain], key)
139             fields.append(_pending[key][domain])
140             sql_string += f" {key} = ?,"
141
142     logger.debug("sql_string(%d)='%s'", len(sql_string), sql_string)
143     if sql_string == "":
144         raise ValueError(f"No fields have been set, but function invoked, domain='{domain}'")
145
146     # Set last_updated to current timestamp
147     fields.append(time.time())
148
149     # For WHERE statement
150     logger.debug("Setting domain='%s' for WHERE statement ...", domain)
151     fields.append(domain)
152
153     logger.debug("sql_string='%s',fields()=%d", sql_string, len(fields))
154     sql_string = "UPDATE instances SET" + sql_string + " last_updated = ? WHERE domain = ? LIMIT 1"
155
156     logger.debug("Executing SQL: sql_string='%s',fields()=%d", sql_string, len(fields))
157     database.cursor.execute(sql_string, fields)
158
159     logger.debug("rowcount=%d", database.cursor.rowcount)
160     if database.cursor.rowcount == 0:
161         raise Exception(f"Did not update any rows: domain='{domain}',fields()={len(fields)}")
162
163     logger.debug("Invoking commit() ...")
164     database.connection.commit()
165
166     logger.debug("Deleting _pending for domain='%s'", domain)
167     for key in _pending:
168         logger.debug("domain='%s',key='%s'", domain, key)
169         if domain in _pending[key]:
170             logger.debug("Deleting key='%s',domain='%s' ...", key, domain)
171             del _pending[key][domain]
172
173     logger.debug("EXIT!")
174
175 def add(domain: str, origin: str, command: str, path: str = None, software: str = None):
176     logger.debug("domain='%s',origin='%s',command='%s',path='%s',software='%s' - CALLED!", domain, origin, command, path, software)
177     domain_helper.raise_on(domain)
178
179     if not isinstance(origin, str) and origin is not None:
180         raise ValueError(f"origin[]='{type(origin)}' is not of type 'str'")
181     elif origin == "":
182         raise ValueError("Parameter 'origin' is empty")
183     elif not isinstance(command, str):
184         raise ValueError(f"command[]='{type(command)}' is not of type 'str'")
185     elif command == "":
186         raise ValueError("Parameter 'command' is empty")
187     elif not isinstance(path, str) and path is not None:
188         raise ValueError(f"path[]='{type(path)}' is not of type 'str'")
189     elif path == "":
190         raise ValueError("Parameter 'path' is empty")
191     elif path is not None and not path.startswith("/"):
192         raise ValueError(f"path='{path}' does not start with / but should")
193     elif not isinstance(software, str) and software is not None:
194         raise ValueError(f"software[]='{type(software)}' is not of type 'str'")
195     elif software == "":
196         raise ValueError("Parameter 'software' is empty")
197     elif origin is not None and not validators.domain(origin.split("/")[0]):
198         raise ValueError(f"Bad origin name='{origin}'")
199     elif blacklist.is_blacklisted(domain):
200         raise Exception(f"domain='{domain}' is blacklisted, but function invoked")
201     elif domain.find("/profile/") > 0 or domain.find("/users/") > 0 or (is_registered(domain.split("/")[0]) and domain.find("/c/") > 0):
202         raise Exception(f"domain='{domain}' is a single user")
203     elif domain.find("/tag/") > 0:
204         raise Exception(f"domain='{domain}' is a tag")
205
206     if software is None:
207         try:
208             logger.debug("domain='%s',origin='%s',command='%s',path='%s'", domain, origin, command, path)
209             software = federation.determine_software(domain, path)
210         except network.exceptions as exception:
211             logger.warning("Exception '%s' during determining software type, domain='%s'", type(exception), domain)
212             set_last_error(domain, exception)
213
214     logger.debug("Determined software='%s'", software)
215     if software == "lemmy" and domain.find("/c/") > 0:
216         domain = domain.split("/c/")[0]
217
218         logger.debug("domain='%s' - LEMMY /c/ !", domain)
219         if is_registered(domain):
220             logger.warning("domain='%s' already registered after cutting off user part. - EXIT!", domain)
221             return
222
223     logger.info("Adding instance domain='%s',origin='%s',software='%s',command='%s' ...", domain, origin, software, command)
224     database.cursor.execute(
225         "INSERT INTO instances (domain, origin, command, hash, software, first_seen) VALUES (?, ?, ?, ?, ?, ?)",
226         (
227            domain,
228            origin,
229            command,
230            utils.get_hash(domain),
231            software,
232            time.time()
233         ),
234     )
235
236     logger.debug("Marking domain='%s' as registered.", domain)
237     cache.set_sub_key("is_registered", domain, True)
238
239     logger.debug("Checking if domain='%s' has pending updates ...", domain)
240     if has_pending(domain):
241         logger.debug("Flushing updates for domain='%s' ...", domain)
242         update(domain)
243
244     logger.debug("EXIT!")
245
246 def set_last_nodeinfo(domain: str):
247     logger.debug("domain='%s' - CALLED!", domain)
248     domain_helper.raise_on(domain)
249
250     logger.debug("Updating last_nodeinfo for domain='%s'", domain)
251     _set_data("last_nodeinfo", domain, time.time())
252
253     logger.debug("EXIT!")
254
255 def set_last_error(domain: str, error: dict):
256     logger.debug("domain='%s',error[]='%s' - CALLED!", domain, type(error))
257     domain_helper.raise_on(domain)
258
259     logger.debug("error[]='%s' - BEFORE!", type(error))
260     if isinstance(error, (BaseException, json.decoder.JSONDecodeError)):
261         error = f"error[{type(error)}]='{str(error)}'"
262     logger.debug("error[]='%s' - AFTER!", type(error))
263
264     if isinstance(error, str):
265         logger.debug("Setting last_error_details='%s' (str)", error)
266         _set_data("last_status_code"  , domain, 999)
267         _set_data("last_error_details", domain, error if error != "" else None)
268     elif isinstance(error, requests.models.Response):
269         logger.debug("Setting last_error_details='%s' (Response)", error.reason)
270         _set_data("last_status_code"  , domain, error.status_code)
271         _set_data("last_error_details", domain, error.reason if error.reason != "" else None)
272     elif not isinstance(error, dict):
273         raise KeyError(f"Cannot handle keys in error[{type(error)}]='{error}'")
274     elif "status_code" in error and "error_message" in error:
275         logger.debug("Setting last_error_details='%s' (error_message)", error['error_message'])
276         _set_data("last_status_code"  , domain, error["status_code"])
277         _set_data("last_error_details", domain, error["error_message"] if error["error_message"] != "" else None)
278     elif "json" in error and "error" in error["json"] and "msg" in error["json"]:
279         logger.debug("Setting last_error_details='%s' (json,error)", error["json"]["msg"])
280         _set_data("last_status_code"  , domain, error["status_code"])
281         _set_data("last_error_details", domain, error["json"]["msg"] if error["json"]["msg"] != "" else None)
282     elif "json" in error and "error" in error["json"] and "message" in error["json"]["error"]:
283         logger.debug("Setting last_error_details='%s' (json,error)", error["json"]["error"]["message"])
284         _set_data("last_status_code"  , domain, error["status_code"])
285         _set_data("last_error_details", domain, error["json"]["error"]["message"] if error["json"]["error"]["message"] != "" else None)
286     elif "json" in error and "error" in error["json"]:
287         logger.debug("Setting last_error_details='%s' (json,error)", error["json"]["error"])
288         _set_data("last_status_code"  , domain, error["status_code"])
289         _set_data("last_error_details", domain, error["json"]["error"] if error["json"]["error"] != "" else None)
290
291     logger.debug("Invoking error_log.add(domain='%s',error[]='%s'", domain, type(error))
292     error_log.add(domain, error)
293
294     logger.debug("EXIT!")
295
296 def set_success(domain: str):
297     logger.debug("domain='%s' - CALLED!", domain)
298     domain_helper.raise_on(domain)
299
300     # Set both to success
301     _set_data("last_status_code"  , domain, 200)
302     _set_data("last_error_details", domain, None)
303
304     logger.debug("EXIT!")
305
306 def is_registered(domain: str, skip_raise = False) -> bool:
307     logger.debug("domain='%s',skip_raise='%s' - CALLED!", domain, skip_raise)
308     domain_helper.raise_on(domain)
309
310     if not isinstance(skip_raise, bool):
311         raise ValueError(f"skip_raise[]='{type(skip_raise)}' is not type of 'bool'")
312
313     if not skip_raise:
314         domain_helper.raise_on(domain)
315
316     logger.debug("domain='%s' - CALLED!", domain)
317     if not cache.key_exists("is_registered"):
318         logger.debug("Cache for 'is_registered' not initialized, fetching all rows ...")
319         database.cursor.execute("SELECT domain FROM instances")
320
321         # Check Set all
322         cache.set_all("is_registered", database.cursor.fetchall(), True)
323
324     # Is cache found?
325     registered = cache.sub_key_exists("is_registered", domain)
326
327     logger.debug("registered='%s' - EXIT!", registered)
328     return registered
329
330 def is_recent(domain: str, column: str = "last_instance_fetch") -> bool:
331     logger.debug("domain='%s',column='%s' - CALLED!", domain, column)
332     domain_helper.raise_on(domain)
333
334     if not isinstance(column, str):
335         raise ValueError(f"Parameter column[]='{type(column)}' is not of type 'str'")
336     elif not column.startswith("last_"):
337         raise ValueError(f"Parameter column='{column}' is not expected")
338     elif not is_registered(domain):
339         logger.debug("domain='%s' is not registered, returning False - EXIT!", domain)
340         return False
341
342     key = "recheck_instance"
343     if column == "last_blocked":
344         key = "recheck_block"
345
346     # Query database
347     database.cursor.execute(f"SELECT {column} FROM instances WHERE domain = ? LIMIT 1", [domain])
348
349     # Fetch row
350     row = database.cursor.fetchone()
351
352     fetched = float(row[column]) if row[column] is not None else 0.0
353
354     diff = (time.time() - fetched)
355
356     logger.debug("fetched[%s]='%s',key='%s',diff=%f", type(fetched), fetched, key, diff)
357     recently = bool(diff < config.get(key))
358
359     logger.debug("recently='%s' - EXIT!", recently)
360     return recently
361
362 def deobfuscate(char: str, domain: str, blocked_hash: str = None) -> tuple:
363     logger.debug("char='%s',domain='%s',blocked_hash='%s' - CALLED!", char, domain, blocked_hash)
364
365     if not isinstance(char, str):
366         raise ValueError(f"Parameter char[]='{type(char)}' is not of type 'str'")
367     elif char == "":
368         raise ValueError("Parameter 'char' is empty")
369     elif not char in domain:
370         raise ValueError(f"char='{char}' not found in domain='{domain}' but function invoked")
371     elif not isinstance(domain, str):
372         raise ValueError(f"Parameter domain[]='{type(domain)}'")
373     elif not isinstance(blocked_hash, str) and blocked_hash is not None:
374         raise ValueError(f"Parameter blocked_hash[]='{type(blocked_hash)}' is not of type 'str'")
375
376     # Init row
377     row = None
378
379     logger.debug("blocked_hash[]='%s'", type(blocked_hash))
380     if isinstance(blocked_hash, str):
381         logger.debug("Looking up blocked_hash='%s',domain='%s' ...", blocked_hash, domain)
382         database.cursor.execute(
383             "SELECT domain, origin, nodeinfo_url FROM instances WHERE hash = ? OR domain LIKE ? LIMIT 1", [blocked_hash, domain.replace(char, "_")]
384         )
385
386         row = database.cursor.fetchone()
387         logger.debug("row[]='%s'", type(row))
388
389         if row is None:
390             logger.debug("blocked_hash='%s' not found, trying domain='%s' ...", blocked_hash, domain)
391             return deobfuscate(char, domain)
392     elif not domain.startswith("*."):
393         logger.debug("domain='%s' - BEFORE!", domain)
394         domain = tidyup.domain(domain)
395         logger.debug("domain='%s' - AFTER!", domain)
396
397         if domain == "":
398             logger.warning("domain is empty after tidyup - EXIT!")
399             return None
400
401         search = domain.replace(char, "_")
402
403         logger.debug("Looking up domain='%s',search='%s' ...", domain, search)
404         database.cursor.execute(
405             "SELECT domain, origin, nodeinfo_url FROM instances WHERE domain LIKE ? OR 'https://' || domain LIKE ? ORDER BY rowid LIMIT 1", [search, search]
406         )
407
408         row = database.cursor.fetchone()
409         logger.debug("row[]='%s'", type(row))
410
411     logger.debug("row[]='%s' - EXIT!", type(row))
412     return row
413
414 def set_last_blocked(domain: str):
415     logger.debug("domain='%s' - CALLED!", domain)
416     domain_helper.raise_on(domain)
417
418     # Set timestamp
419     _set_data("last_blocked", domain, time.time())
420     logger.debug("EXIT!")
421
422 def set_last_instance_fetch(domain: str):
423     logger.debug("domain='%s' - CALLED!", domain)
424     domain_helper.raise_on(domain)
425
426     # Set timestamp
427     _set_data("last_instance_fetch", domain, time.time())
428     logger.debug("EXIT!")
429
430 def set_last_response_time(domain: str, response_time: float):
431     logger.debug("domain='%s',response_time=%d - CALLED!", domain, response_time)
432     domain_helper.raise_on(domain)
433
434     if not isinstance(response_time, float):
435         raise ValueError(f"response_time[]='{type(response_time)}' is not of type 'float'")
436     elif response_time < 0:
437         raise ValueError(f"response_time={response_time} is below zero")
438
439     # Set timestamp
440     _set_data("last_response_time", domain, response_time)
441     logger.debug("EXIT!")
442
443 def set_total_peers(domain: str, peers: list):
444     logger.debug("domain='%s',peers()=%d - CALLED!", domain, len(peers))
445     domain_helper.raise_on(domain)
446
447     if not isinstance(peers, list):
448         raise ValueError(f"Parameter peers[]='{type(peers)}' is not of type 'list'")
449
450     # Set timestamp
451     _set_data("total_peers", domain, len(peers))
452     logger.debug("EXIT!")
453
454 def set_total_blocks(domain: str, blocks: list):
455     logger.debug("domain='%s',blocks()=%d - CALLED!", domain, len(blocks))
456     domain_helper.raise_on(domain)
457
458     if not isinstance(blocks, list):
459         raise ValueError(f"Parameter blocks[]='{type(blocks)}' is not of type 'list'")
460
461     # Set timestamp
462     _set_data("total_blocks", domain, len(blocks))
463     logger.debug("EXIT!")
464
465 def set_obfuscated_blocks(domain: str, obfuscated: int):
466     logger.debug("domain='%s',obfuscated=%d - CALLED!", domain, obfuscated)
467     domain_helper.raise_on(domain)
468
469     if not isinstance(obfuscated, int):
470         raise ValueError(f"Parameter obfuscated[]='{type(obfuscated)}' is not of type 'int'")
471     elif obfuscated < 0:
472         raise ValueError(f"Parameter obfuscated={obfuscated} is not valid")
473
474     # Set timestamp
475     _set_data("obfuscated_blocks", domain, obfuscated)
476     logger.debug("EXIT!")
477
478 def set_nodeinfo_url(domain: str, url: str):
479     logger.debug("domain='%s',url='%s' - CALLED!", domain, url)
480     domain_helper.raise_on(domain)
481
482     if not isinstance(url, str) and url is not None:
483         raise ValueError(f"Parameter url[]='{type(url)}' is not of type 'str'")
484     elif url == "":
485         raise ValueError("Parameter 'url' is empty")
486
487     # Set timestamp
488     _set_data("nodeinfo_url", domain, url)
489     logger.debug("EXIT!")
490
491 def set_detection_mode(domain: str, mode: str):
492     logger.debug("domain='%s',mode='%s' - CALLED!", domain, mode)
493     domain_helper.raise_on(domain)
494
495     if not isinstance(mode, str) and mode is not None:
496         raise ValueError(f"Parameter mode[]='{type(mode)}' is not of type 'str'")
497     elif mode == "":
498         raise ValueError("Parameter 'mode' is empty")
499
500     # Set timestamp
501     _set_data("detection_mode", domain, mode)
502     logger.debug("EXIT!")
503
504 def set_has_obfuscation(domain: str, status: bool):
505     logger.debug("domain='%s',status='%s' - CALLED!", domain, status)
506     domain_helper.raise_on(domain)
507
508     if not isinstance(status, bool):
509         raise ValueError(f"Parameter status[]='{type(status)}' is not of type 'bool'")
510
511     # Set timestamp
512     _set_data("has_obfuscation", domain, status)
513     logger.debug("EXIT!")
514
515 def set_original_software(domain: str, software: str):
516     logger.debug("domain='%s',software='%s' - CALLED!", domain, software)
517     domain_helper.raise_on(domain)
518
519     if not isinstance(software, str) and software is not None:
520         raise ValueError(f"Parameter software[]='{type(software)}' is not of type 'str'")
521     elif software == "":
522         raise ValueError("Parameter 'software' is empty")
523
524     # Set original software
525     _set_data("original_software", domain, software)
526     logger.debug("EXIT!")
527
528
529 def set_software(domain: str, software: str):
530     logger.debug("domain='%s',software='%s' - CALLED!", domain, software)
531     domain_helper.raise_on(domain)
532
533     if not isinstance(software, str) and software is not None:
534         raise ValueError(f"Parameter software[]='{type(software)}' is not of type 'str'")
535     elif software == "":
536         raise ValueError("Parameter 'software' is empty")
537
538     # Set software (maybe aliased to generic name)
539     _set_data("software", domain, software)
540     logger.debug("EXIT!")
541
542 def valid(value: str, column: str) -> bool:
543     logger.debug("value='%s' - CALLED!", value)
544     if not isinstance(value, str):
545         raise ValueError(f"Parameter value[]='{type(value)}' is not of type 'str'")
546     elif value == "":
547         raise ValueError("Parameter 'value' is empty")
548     elif not isinstance(column, str):
549         raise ValueError(f"Parameter column[]='{type(column)}' is not of type 'str'")
550     elif column == "":
551         raise ValueError("Parameter 'column' is empty")
552
553     # Query database
554     database.cursor.execute(
555         f"SELECT {column} FROM instances WHERE {column} = ? LIMIT 1", [value]
556     )
557
558     is_valid = database.cursor.fetchone() is not None
559
560     logger.debug("is_valid='%s' - EXIT!", is_valid)
561     return is_valid
562
563 def translate_idnas(rows: list, column: str):
564     logger.debug("rows[]='%s' - CALLED!", type(rows))
565
566     if not isinstance(rows, list):
567         raise ValueError("rows[]='{type(rows)}' is not of type 'list'")
568     elif len(rows) == 0:
569         raise ValueError("Parameter 'rows' is an empty list")
570     elif not isinstance(column, str):
571         raise ValueError(f"column='{type(column)}' is not of type 'str'")
572     elif column == "":
573         raise ValueError("Parameter 'column' is empty")
574     elif column not in ["domain", "origin"]:
575         raise ValueError(f"column='{column}' is not supported")
576
577     logger.info("Checking/converting %d domain names ...", len(rows))
578     for row in rows:
579         logger.debug("row[]='%s'", type(row))
580
581         translated = row[column].encode("idna").decode("utf-8")
582         logger.debug("translated='%s',row[%s]='%s'", translated, column, row[column])
583
584         if translated != row[column]:
585             logger.info("Translated row[%s]='%s' to '%s'", column, row[column], translated)
586             if is_registered(translated, True):
587                 logger.warning("Deleting row[%s]='%s' as translated='%s' already exist", column, row[column], translated)
588                 database.cursor.execute(f"DELETE FROM instances WHERE {column} = ? LIMIT 1", [row[column]])
589             else:
590                 logger.debug("Updating row[%s]='%s' to translated='%s' ...", column, row[column], translated)
591                 database.cursor.execute(f"UPDATE instances SET {column} = ? WHERE {column} = ? LIMIT 1", [translated, row[column]])
592
593             logger.debug("Invoking commit() ...")
594             database.connection.commit()
595
596     logger.debug("EXIT!")