fded6ce46cb167faaae559ff93b050c2b7d18ff1
max
  Mon Jun 26 08:59:00 2023 -0700
Porting hgGeneGraph to python3. refs #31563

diff --git src/hg/pyLib/pymysql/converters.py src/hg/pyLib/pymysql/converters.py
new file mode 100644
index 0000000..d910f5c
--- /dev/null
+++ src/hg/pyLib/pymysql/converters.py
@@ -0,0 +1,364 @@
+import datetime
+from decimal import Decimal
+import re
+import time
+
+from .err import ProgrammingError
+from .constants import FIELD_TYPE
+
+
+def escape_item(val, charset, mapping=None):
+    if mapping is None:
+        mapping = encoders
+    encoder = mapping.get(type(val))
+
+    # Fallback to default when no encoder found
+    if not encoder:
+        try:
+            encoder = mapping[str]
+        except KeyError:
+            raise TypeError("no default type converter defined")
+
+    if encoder in (escape_dict, escape_sequence):
+        val = encoder(val, charset, mapping)
+    else:
+        val = encoder(val, mapping)
+    return val
+
+
+def escape_dict(val, charset, mapping=None):
+    n = {}
+    for k, v in val.items():
+        quoted = escape_item(v, charset, mapping)
+        n[k] = quoted
+    return n
+
+
+def escape_sequence(val, charset, mapping=None):
+    n = []
+    for item in val:
+        quoted = escape_item(item, charset, mapping)
+        n.append(quoted)
+    return "(" + ",".join(n) + ")"
+
+
+def escape_set(val, charset, mapping=None):
+    return ",".join([escape_item(x, charset, mapping) for x in val])
+
+
+def escape_bool(value, mapping=None):
+    return str(int(value))
+
+
+def escape_int(value, mapping=None):
+    return str(value)
+
+
+def escape_float(value, mapping=None):
+    s = repr(value)
+    if s in ("inf", "nan"):
+        raise ProgrammingError("%s can not be used with MySQL" % s)
+    if "e" not in s:
+        s += "e0"
+    return s
+
+
+_escape_table = [chr(x) for x in range(128)]
+_escape_table[0] = "\\0"
+_escape_table[ord("\\")] = "\\\\"
+_escape_table[ord("\n")] = "\\n"
+_escape_table[ord("\r")] = "\\r"
+_escape_table[ord("\032")] = "\\Z"
+_escape_table[ord('"')] = '\\"'
+_escape_table[ord("'")] = "\\'"
+
+
+def escape_string(value, mapping=None):
+    """escapes *value* without adding quote.
+
+    Value should be unicode
+    """
+    return value.translate(_escape_table)
+
+
+def escape_bytes_prefixed(value, mapping=None):
+    return "_binary'%s'" % value.decode("ascii", "surrogateescape").translate(
+        _escape_table
+    )
+
+
+def escape_bytes(value, mapping=None):
+    return "'%s'" % value.decode("ascii", "surrogateescape").translate(_escape_table)
+
+
+def escape_str(value, mapping=None):
+    return "'%s'" % escape_string(str(value), mapping)
+
+
+def escape_None(value, mapping=None):
+    return "NULL"
+
+
+def escape_timedelta(obj, mapping=None):
+    seconds = int(obj.seconds) % 60
+    minutes = int(obj.seconds // 60) % 60
+    hours = int(obj.seconds // 3600) % 24 + int(obj.days) * 24
+    if obj.microseconds:
+        fmt = "'{0:02d}:{1:02d}:{2:02d}.{3:06d}'"
+    else:
+        fmt = "'{0:02d}:{1:02d}:{2:02d}'"
+    return fmt.format(hours, minutes, seconds, obj.microseconds)
+
+
+def escape_time(obj, mapping=None):
+    if obj.microsecond:
+        fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'"
+    else:
+        fmt = "'{0.hour:02}:{0.minute:02}:{0.second:02}'"
+    return fmt.format(obj)
+
+
+def escape_datetime(obj, mapping=None):
+    if obj.microsecond:
+        fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}.{0.microsecond:06}'"
+    else:
+        fmt = "'{0.year:04}-{0.month:02}-{0.day:02} {0.hour:02}:{0.minute:02}:{0.second:02}'"
+    return fmt.format(obj)
+
+
+def escape_date(obj, mapping=None):
+    fmt = "'{0.year:04}-{0.month:02}-{0.day:02}'"
+    return fmt.format(obj)
+
+
+def escape_struct_time(obj, mapping=None):
+    return escape_datetime(datetime.datetime(*obj[:6]))
+
+
+def Decimal2Literal(o, d):
+    return format(o, "f")
+
+
+def _convert_second_fraction(s):
+    if not s:
+        return 0
+    # Pad zeros to ensure the fraction length in microseconds
+    s = s.ljust(6, "0")
+    return int(s[:6])
+
+
+DATETIME_RE = re.compile(
+    r"(\d{1,4})-(\d{1,2})-(\d{1,2})[T ](\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?"
+)
+
+
+def convert_datetime(obj):
+    """Returns a DATETIME or TIMESTAMP column value as a datetime object:
+
+      >>> datetime_or_None('2007-02-25 23:06:20')
+      datetime.datetime(2007, 2, 25, 23, 6, 20)
+      >>> datetime_or_None('2007-02-25T23:06:20')
+      datetime.datetime(2007, 2, 25, 23, 6, 20)
+
+    Illegal values are returned as None:
+
+      >>> datetime_or_None('2007-02-31T23:06:20') is None
+      True
+      >>> datetime_or_None('0000-00-00 00:00:00') is None
+      True
+
+    """
+    if isinstance(obj, (bytes, bytearray)):
+        obj = obj.decode("ascii")
+
+    m = DATETIME_RE.match(obj)
+    if not m:
+        return convert_date(obj)
+
+    try:
+        groups = list(m.groups())
+        groups[-1] = _convert_second_fraction(groups[-1])
+        return datetime.datetime(*[int(x) for x in groups])
+    except ValueError:
+        return convert_date(obj)
+
+
+TIMEDELTA_RE = re.compile(r"(-)?(\d{1,3}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?")
+
+
+def convert_timedelta(obj):
+    """Returns a TIME column as a timedelta object:
+
+      >>> timedelta_or_None('25:06:17')
+      datetime.timedelta(1, 3977)
+      >>> timedelta_or_None('-25:06:17')
+      datetime.timedelta(-2, 83177)
+
+    Illegal values are returned as None:
+
+      >>> timedelta_or_None('random crap') is None
+      True
+
+    Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but
+    can accept values as (+|-)DD HH:MM:SS. The latter format will not
+    be parsed correctly by this function.
+    """
+    if isinstance(obj, (bytes, bytearray)):
+        obj = obj.decode("ascii")
+
+    m = TIMEDELTA_RE.match(obj)
+    if not m:
+        return obj
+
+    try:
+        groups = list(m.groups())
+        groups[-1] = _convert_second_fraction(groups[-1])
+        negate = -1 if groups[0] else 1
+        hours, minutes, seconds, microseconds = groups[1:]
+
+        tdelta = (
+            datetime.timedelta(
+                hours=int(hours),
+                minutes=int(minutes),
+                seconds=int(seconds),
+                microseconds=int(microseconds),
+            )
+            * negate
+        )
+        return tdelta
+    except ValueError:
+        return obj
+
+
+TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})(?:.(\d{1,6}))?")
+
+
+def convert_time(obj):
+    """Returns a TIME column as a time object:
+
+      >>> time_or_None('15:06:17')
+      datetime.time(15, 6, 17)
+
+    Illegal values are returned as None:
+
+      >>> time_or_None('-25:06:17') is None
+      True
+      >>> time_or_None('random crap') is None
+      True
+
+    Note that MySQL always returns TIME columns as (+|-)HH:MM:SS, but
+    can accept values as (+|-)DD HH:MM:SS. The latter format will not
+    be parsed correctly by this function.
+
+    Also note that MySQL's TIME column corresponds more closely to
+    Python's timedelta and not time. However if you want TIME columns
+    to be treated as time-of-day and not a time offset, then you can
+    use set this function as the converter for FIELD_TYPE.TIME.
+    """
+    if isinstance(obj, (bytes, bytearray)):
+        obj = obj.decode("ascii")
+
+    m = TIME_RE.match(obj)
+    if not m:
+        return obj
+
+    try:
+        groups = list(m.groups())
+        groups[-1] = _convert_second_fraction(groups[-1])
+        hours, minutes, seconds, microseconds = groups
+        return datetime.time(
+            hour=int(hours),
+            minute=int(minutes),
+            second=int(seconds),
+            microsecond=int(microseconds),
+        )
+    except ValueError:
+        return obj
+
+
+def convert_date(obj):
+    """Returns a DATE column as a date object:
+
+      >>> date_or_None('2007-02-26')
+      datetime.date(2007, 2, 26)
+
+    Illegal values are returned as None:
+
+      >>> date_or_None('2007-02-31') is None
+      True
+      >>> date_or_None('0000-00-00') is None
+      True
+
+    """
+    if isinstance(obj, (bytes, bytearray)):
+        obj = obj.decode("ascii")
+    try:
+        return datetime.date(*[int(x) for x in obj.split("-", 2)])
+    except ValueError:
+        return obj
+
+
+def through(x):
+    return x
+
+
+# def convert_bit(b):
+#    b = "\x00" * (8 - len(b)) + b # pad w/ zeroes
+#    return struct.unpack(">Q", b)[0]
+#
+#     the snippet above is right, but MySQLdb doesn't process bits,
+#     so we shouldn't either
+convert_bit = through
+
+
+encoders = {
+    bool: escape_bool,
+    int: escape_int,
+    float: escape_float,
+    str: escape_str,
+    bytes: escape_bytes,
+    tuple: escape_sequence,
+    list: escape_sequence,
+    set: escape_sequence,
+    frozenset: escape_sequence,
+    dict: escape_dict,
+    type(None): escape_None,
+    datetime.date: escape_date,
+    datetime.datetime: escape_datetime,
+    datetime.timedelta: escape_timedelta,
+    datetime.time: escape_time,
+    time.struct_time: escape_struct_time,
+    Decimal: Decimal2Literal,
+}
+
+
+decoders = {
+    FIELD_TYPE.BIT: convert_bit,
+    FIELD_TYPE.TINY: int,
+    FIELD_TYPE.SHORT: int,
+    FIELD_TYPE.LONG: int,
+    FIELD_TYPE.FLOAT: float,
+    FIELD_TYPE.DOUBLE: float,
+    FIELD_TYPE.LONGLONG: int,
+    FIELD_TYPE.INT24: int,
+    FIELD_TYPE.YEAR: int,
+    FIELD_TYPE.TIMESTAMP: convert_datetime,
+    FIELD_TYPE.DATETIME: convert_datetime,
+    FIELD_TYPE.TIME: convert_timedelta,
+    FIELD_TYPE.DATE: convert_date,
+    FIELD_TYPE.BLOB: through,
+    FIELD_TYPE.TINY_BLOB: through,
+    FIELD_TYPE.MEDIUM_BLOB: through,
+    FIELD_TYPE.LONG_BLOB: through,
+    FIELD_TYPE.STRING: through,
+    FIELD_TYPE.VAR_STRING: through,
+    FIELD_TYPE.VARCHAR: through,
+    FIELD_TYPE.DECIMAL: Decimal,
+    FIELD_TYPE.NEWDECIMAL: Decimal,
+}
+
+
+# for MySQLdb compatibility
+conversions = encoders.copy()
+conversions.update(decoders)
+Thing2Literal = escape_str