3 Utility classes and functions.
5 ------------------------------------------------------------------------------
6 This file is part of dblite - simple query interface for SQL databases.
7 Released under the MIT License.
12 ------------------------------------------------------------------------------
27 logger = logging.getLogger(__name__)
31 """datetime.tzinfo class representing a constant offset from UTC."""
32 ZERO = datetime.timedelta(0)
35 """Constructs a new static zone info, with specified name and time delta."""
40 def dst(self, dt):
return self.ZERO
41 def tzname(self, dt):
return self._name
43 def __repr__(self):
return "%s(%s)" % (self.__class__.__name__, self.
_name)
45 return isinstance(other, self.__class__)
and self.
_offset == other._offset
53 Returns object constructed with data dictionary.
55 @param ctor callable like a class, declared args are matched case-insensitively
56 for positional arguments if keyword argument invocation fails
57 @param data data dictionary with string keys
58 @return (result, [error strings])
61 try:
return ctor(**data), []
62 except Exception
as e: errors.append(e)
63 try:
return ctor(*data.values()), []
64 except Exception
as e: errors.append(e)
65 try:
return ctor(data), []
66 except Exception
as e: errors.append(e)
68 try:
return ctor(**dict({k:
None for k
in ctor._fields}, **data)), []
69 except Exception
as e: errors.append(e)
70 try:
return ctor(*map(data.get, ctor._fields)), []
71 except Exception
as e: errors.append(e)
76 """Returns whether input is a data object: namedtuple, or has attributes or slots."""
79 if getattr(obj,
"__slots__",
None):
81 if any(isinstance(v, property)
for _, v
in inspect.getmembers(type(obj))):
83 if getattr(obj,
"__dict__",
None):
89 """Returns whether input is a namedtuple class or instance."""
90 return (isinstance(obj, tuple)
or inspect.isclass(obj)
and issubclass(obj, tuple)) \
91 and hasattr(obj,
"_asdict")
and hasattr(obj,
"_fields")
94 def json_dumps(data, indent=2, sort_keys=True):
96 Returns JSON string, with datetime types converted to ISO-8601 strings
97 (in UTC if no timezone set), sets converted to lists,
98 and Decimal objects converted to float or int. Returns None if data is None.
100 if data
is None:
return None
102 if isinstance(x, set):
return list(x)
103 if isinstance(x, (datetime.datetime, datetime.date, datetime.time)):
104 if x.tzinfo
is None: x = x.replace(tzinfo=UTC)
106 if isinstance(x, decimal.Decimal):
107 return float(x)
if x.as_tuple().exponent
else int(x)
109 return json.dumps(data, default=encoder, indent=indent, sort_keys=sort_keys)
114 Returns deserialized JSON, with datetime/date strings converted to objects.
116 Returns original input if loading as JSON failed.
118 def convert_recursive(data):
119 """Converts ISO datetime strings to objects in nested dicts or lists."""
121 pairs = enumerate(data)
if isinstance(data, list) \
122 else data.items()
if isinstance(data, dict)
else []
123 rgx =
r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(\.\d+)?(([+-]\d{2}:?\d{2})|Z)?$"
125 if isinstance(v, (dict, list)): v = convert_recursive(v)
126 elif isinstance(v, six.string_types)
and len(v) > 18
and re.match(rgx, v):
128 result.append((k, v))
129 return [x
for _, x
in result]
if isinstance(data, list) \
130 else dict(result)
if isinstance(data, dict)
else data
132 return None if s
is None else json.loads(s, object_hook=convert_recursive)
134 fails = getattr(json_loads,
"__fails", set())
135 if hash(s)
not in fails:
136 logger.warning(
"Failed to parse JSON from %r.", s, exc_info=
True)
137 setattr(json_loads,
"__fails", fails | set([hash(s)]))
143 Returns a list of keys and values, or [given object] if not applicable.
145 @param obj mapping or namedtuple or list|set|tuple or object with attributes or slots
146 @param namefmt function(key) to apply on extracted keys, if any
147 @return [(key, value)] if available,
148 else original argument as list if list/set/tuple,
149 else list with a single item
151 namefmt = namefmt
if callable(namefmt)
else lambda x: x
152 if type(obj)
in (dict, collections.defaultdict, collections.OrderedDict):
153 return list(obj.items())
154 if isinstance(obj, (list, set)):
157 return [(namefmt(k), getattr(obj, k))
for k
in obj._fields]
158 if isinstance(obj, tuple):
160 if getattr(obj,
"__slots__",
None):
161 return [(namefmt(k), getattr(obj, k))
for k
in obj.__slots__
163 if any(isinstance(v, property)
for _, v
in inspect.getmembers(type(obj))):
164 return [(namefmt(k), getattr(obj, k))
for k, v
in inspect.getmembers(type(obj))
165 if isinstance(v, property)]
166 if getattr(obj,
"__dict__",
None):
167 return [(namefmt(k), v)
for k, v
in vars(obj).items()]
168 if isinstance(obj, six.moves.collections_abc.Mapping):
169 return list(obj.items())
174 """Returns db engines loaded from file directory, as {name: module}."""
176 for n
in sorted(glob.glob(os.path.join(os.path.dirname(__file__),
"engines",
"*"))):
177 name = os.path.splitext(os.path.basename(n))[0]
178 if name.startswith(
"__")
or os.path.isfile(n)
and not re.match(
".*pyc?$", n) \
179 or os.path.isdir(n)
and not any(glob.glob(os.path.join(n, x))
for x
in (
"*.py",
"*.pyc")):
182 modulename =
"%s.%s.%s" % (__package__,
"engines", name)
183 module = importlib.import_module(modulename)
184 result[name] = module
188 def nameify(val, namefmt=None, parent=None):
190 Returns value as table or column name string.
192 @param val a primitive like string, or a named object like a class,
193 or a class property or member or data descriptor
194 @param namefmt function(name) to apply on name extracted from class or object, if any
195 @param parent the parent class object if value is a class member or property
198 if isinstance(val, six.string_types):
200 namefmt = namefmt
if callable(namefmt)
else lambda x: x
201 if inspect.isclass(val):
202 return namefmt(val.__name__)
203 if inspect.isdatadescriptor(val):
204 if hasattr(val,
"__name__"):
return namefmt(val.__name__)
205 return next(namefmt(k)
for k, v
in inspect.getmembers(parent)
if v
is val)
206 return six.text_type(val)
211 Tries to parse string as ISO8601 datetime, returns input on error.
212 Supports "YYYY-MM-DD[ T]HH:MM:SS(.micros)?(Z|[+-]HH(:MM)?)?".
213 All returned datetimes are timezone-aware, falling back to UTC.
215 if len(s) < 18:
return s
216 rgx =
r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(\.\d+)?(([+-]\d{2}(:?\d{2})?)|Z)?$"
217 result, match = s, re.match(rgx, s)
219 millis, _, offset, _ = match.groups()
220 minimal = re.sub(
r"\D",
"", s[:match.span(2)[0]]
if offset
else s)
221 fmt =
"%Y%m%d%H%M%S" + (
"%f" if millis
else "")
223 result = datetime.datetime.strptime(minimal, fmt)
225 hh, mm = map(int, [offset[1:3], offset[4:]])
226 delta = datetime.timedelta(hours=hh, minutes=mm)
227 if offset.startswith(
"-"): delta = -delta
228 result = result.replace(tzinfo=
StaticTzInfo(offset, delta))
229 except ValueError:
pass
230 if isinstance(result, datetime.datetime)
and result.tzinfo
is None:
231 result = result.replace(tzinfo=UTC)
236 "StaticTzInfo",
"UTC",
237 "factory",
"is_dataobject",
"is_namedtuple",
"json_dumps",
"json_loads",
238 "keyvalues",
"load_modules",
"nameify",
"parse_datetime",