From 9d0fa0017fbd809b8fe21283f1f9af48ef53dc31 Mon Sep 17 00:00:00 2001 From: Zephyre Date: Sun, 20 Sep 2015 20:14:06 +0800 Subject: [PATCH] ADD: extract generation time related information --- alfred.py | 93 ++ bson/__init__.py | 939 ++++++++++++++++++ bson/binary.py | 236 +++++ bson/code.py | 81 ++ bson/codec_options.py | 81 ++ bson/dbref.py | 135 +++ bson/errors.py | 40 + bson/int64.py | 34 + bson/json_util.py | 257 +++++ bson/max_key.py | 47 + bson/min_key.py | 47 + bson/objectid.py | 292 ++++++ bson/py3compat.py | 96 ++ bson/regex.py | 126 +++ bson/son.py | 249 +++++ bson/timestamp.py | 116 +++ bson/tz_util.py | 52 + process.py | 99 ++ tzlocal/__init__.py | 7 + tzlocal/darwin.py | 27 + tzlocal/test_data/Harare | Bin 0 -> 157 bytes tzlocal/test_data/localtime/etc/localtime | Bin 0 -> 157 bytes .../test_data/symlink_localtime/etc/localtime | Bin 0 -> 157 bytes .../usr/share/zoneinfo/Africa/Harare | Bin 0 -> 157 bytes tzlocal/test_data/timezone/etc/timezone | 1 + .../timezone_setting/etc/conf.d/clock | 1 + .../zone_setting/etc/sysconfig/clock | 1 + tzlocal/tests.py | 70 ++ tzlocal/unix.py | 129 +++ tzlocal/win32.py | 93 ++ tzlocal/windows_tz.py | 542 ++++++++++ 31 files changed, 3891 insertions(+) create mode 100755 alfred.py create mode 100644 bson/__init__.py create mode 100644 bson/binary.py create mode 100644 bson/code.py create mode 100644 bson/codec_options.py create mode 100644 bson/dbref.py create mode 100644 bson/errors.py create mode 100644 bson/int64.py create mode 100644 bson/json_util.py create mode 100644 bson/max_key.py create mode 100644 bson/min_key.py create mode 100644 bson/objectid.py create mode 100644 bson/py3compat.py create mode 100644 bson/regex.py create mode 100644 bson/son.py create mode 100644 bson/timestamp.py create mode 100644 bson/tz_util.py create mode 100644 process.py create mode 100644 tzlocal/__init__.py create mode 100644 tzlocal/darwin.py create mode 100644 tzlocal/test_data/Harare create mode 100644 tzlocal/test_data/localtime/etc/localtime create mode 100644 tzlocal/test_data/symlink_localtime/etc/localtime create mode 100644 tzlocal/test_data/symlink_localtime/usr/share/zoneinfo/Africa/Harare create mode 100644 tzlocal/test_data/timezone/etc/timezone create mode 100644 tzlocal/test_data/timezone_setting/etc/conf.d/clock create mode 100644 tzlocal/test_data/zone_setting/etc/sysconfig/clock create mode 100644 tzlocal/tests.py create mode 100644 tzlocal/unix.py create mode 100644 tzlocal/win32.py create mode 100644 tzlocal/windows_tz.py diff --git a/alfred.py b/alfred.py new file mode 100755 index 0000000..144fce9 --- /dev/null +++ b/alfred.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +import itertools +import os +import plistlib +import unicodedata +import sys + +from xml.etree.ElementTree import Element, SubElement, tostring + +""" +You should run your script via /bin/bash with all escape options ticked. +The command line should be + +python yourscript.py "{query}" arg2 arg3 ... +""" +UNESCAPE_CHARACTERS = u""" ;()""" + +_MAX_RESULTS_DEFAULT = 9 + +preferences = plistlib.readPlist('info.plist') +bundleid = preferences['bundleid'] + +class Item(object): + @classmethod + def unicode(cls, value): + try: + items = value.iteritems() + except AttributeError: + return unicode(value) + else: + return dict(map(unicode, item) for item in items) + + def __init__(self, attributes, title, subtitle, icon=None): + self.attributes = attributes + self.title = title + self.subtitle = subtitle + self.icon = icon + + def __str__(self): + return tostring(self.xml(), encoding='utf-8') + + def xml(self): + item = Element(u'item', self.unicode(self.attributes)) + for attribute in (u'title', u'subtitle', u'icon'): + value = getattr(self, attribute) + if value is None: + continue + try: + (value, attributes) = value + except: + attributes = {} + SubElement(item, attribute, self.unicode(attributes)).text = unicode(value) + return item + +def args(characters=None): + return tuple(unescape(decode(arg), characters) for arg in sys.argv[1:]) + +def config(): + return _create('config') + +def decode(s): + return unicodedata.normalize('NFC', s.decode('utf-8')) + +def uid(uid): + return u'-'.join(map(unicode, (bundleid, uid))) + +def unescape(query, characters=None): + for character in (UNESCAPE_CHARACTERS if (characters is None) else characters): + query = query.replace('\\%s' % character, character) + return query + +def work(volatile): + path = { + True: '~/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data', + False: '~/Library/Application Support/Alfred 2/Workflow Data' + }[bool(volatile)] + return _create(os.path.join(os.path.expanduser(path), bundleid)) + +def write(text): + sys.stdout.write(text) + +def xml(items, maxresults=_MAX_RESULTS_DEFAULT): + root = Element('items') + for item in itertools.islice(items, maxresults): + root.append(item.xml()) + return tostring(root, encoding='utf-8') + +def _create(path): + if not os.path.isdir(path): + os.mkdir(path) + if not os.access(path, os.W_OK): + raise IOError('No write access: %s' % path) + return path diff --git a/bson/__init__.py b/bson/__init__.py new file mode 100644 index 0000000..77bae03 --- /dev/null +++ b/bson/__init__.py @@ -0,0 +1,939 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""BSON (Binary JSON) encoding and decoding. +""" + +import calendar +import collections +import datetime +import itertools +import re +import struct +import sys +import uuid + +from codecs import (utf_8_decode as _utf_8_decode, + utf_8_encode as _utf_8_encode) + +from bson.binary import (Binary, OLD_UUID_SUBTYPE, + JAVA_LEGACY, CSHARP_LEGACY, + UUIDLegacy) +from bson.code import Code +from bson.codec_options import CodecOptions, DEFAULT_CODEC_OPTIONS +from bson.dbref import DBRef +from bson.errors import (InvalidBSON, + InvalidDocument, + InvalidStringData) +from bson.int64 import Int64 +from bson.max_key import MaxKey +from bson.min_key import MinKey +from bson.objectid import ObjectId +from bson.py3compat import (b, + PY3, + iteritems, + text_type, + string_type, + reraise) +from bson.regex import Regex +from bson.son import SON, RE_TYPE +from bson.timestamp import Timestamp +from bson.tz_util import utc + + +try: + from bson import _cbson + _USE_C = True +except ImportError: + _USE_C = False + + +EPOCH_AWARE = datetime.datetime.fromtimestamp(0, utc) +EPOCH_NAIVE = datetime.datetime.utcfromtimestamp(0) + + +BSONNUM = b"\x01" # Floating point +BSONSTR = b"\x02" # UTF-8 string +BSONOBJ = b"\x03" # Embedded document +BSONARR = b"\x04" # Array +BSONBIN = b"\x05" # Binary +BSONUND = b"\x06" # Undefined +BSONOID = b"\x07" # ObjectId +BSONBOO = b"\x08" # Boolean +BSONDAT = b"\x09" # UTC Datetime +BSONNUL = b"\x0A" # Null +BSONRGX = b"\x0B" # Regex +BSONREF = b"\x0C" # DBRef +BSONCOD = b"\x0D" # Javascript code +BSONSYM = b"\x0E" # Symbol +BSONCWS = b"\x0F" # Javascript code with scope +BSONINT = b"\x10" # 32bit int +BSONTIM = b"\x11" # Timestamp +BSONLON = b"\x12" # 64bit int +BSONMIN = b"\xFF" # Min key +BSONMAX = b"\x7F" # Max key + + +_UNPACK_FLOAT = struct.Struct("= obj_end: + raise InvalidBSON("invalid object length") + obj = _elements_to_dict(data, position + 4, end, opts) + + position += obj_size + if "$ref" in obj: + return (DBRef(obj.pop("$ref"), obj.pop("$id", None), + obj.pop("$db", None), obj), position) + return obj, position + + +def _get_array(data, position, obj_end, opts): + """Decode a BSON array to python list.""" + size = _UNPACK_INT(data[position:position + 4])[0] + end = position + size - 1 + if data[end:end + 1] != b"\x00": + raise InvalidBSON("bad eoo") + position += 4 + end -= 1 + result = [] + + # Avoid doing global and attibute lookups in the loop. + append = result.append + index = data.index + getter = _ELEMENT_GETTER + + while position < end: + element_type = data[position:position + 1] + # Just skip the keys. + position = index(b'\x00', position) + 1 + value, position = getter[element_type](data, position, obj_end, opts) + append(value) + return result, position + 1 + + +def _get_binary(data, position, dummy, opts): + """Decode a BSON binary to bson.binary.Binary or python UUID.""" + length, subtype = _UNPACK_LENGTH_SUBTYPE(data[position:position + 5]) + position += 5 + if subtype == 2: + length2 = _UNPACK_INT(data[position:position + 4])[0] + position += 4 + if length2 != length - 4: + raise InvalidBSON("invalid binary (st 2) - lengths don't match!") + length = length2 + end = position + length + if subtype in (3, 4): + # Java Legacy + uuid_representation = opts.uuid_representation + if uuid_representation == JAVA_LEGACY: + java = data[position:end] + value = uuid.UUID(bytes=java[0:8][::-1] + java[8:16][::-1]) + # C# legacy + elif uuid_representation == CSHARP_LEGACY: + value = uuid.UUID(bytes_le=data[position:end]) + # Python + else: + value = uuid.UUID(bytes=data[position:end]) + return value, end + # Python3 special case. Decode subtype 0 to 'bytes'. + if PY3 and subtype == 0: + value = data[position:end] + else: + value = Binary(data[position:end], subtype) + return value, end + + +def _get_oid(data, position, dummy0, dummy1): + """Decode a BSON ObjectId to bson.objectid.ObjectId.""" + end = position + 12 + return ObjectId(data[position:end]), end + + +def _get_boolean(data, position, dummy0, dummy1): + """Decode a BSON true/false to python True/False.""" + end = position + 1 + return data[position:end] == b"\x01", end + + +def _get_date(data, position, dummy, opts): + """Decode a BSON datetime to python datetime.datetime.""" + end = position + 8 + millis = _UNPACK_LONG(data[position:end])[0] + diff = ((millis % 1000) + 1000) % 1000 + seconds = (millis - diff) / 1000 + micros = diff * 1000 + if opts.tz_aware: + return EPOCH_AWARE + datetime.timedelta( + seconds=seconds, microseconds=micros), end + else: + return EPOCH_NAIVE + datetime.timedelta( + seconds=seconds, microseconds=micros), end + + +def _get_code(data, position, obj_end, opts): + """Decode a BSON code to bson.code.Code.""" + code, position = _get_string(data, position, obj_end, opts) + return Code(code), position + + +def _get_code_w_scope(data, position, obj_end, opts): + """Decode a BSON code_w_scope to bson.code.Code.""" + code, position = _get_string(data, position + 4, obj_end, opts) + scope, position = _get_object(data, position, obj_end, opts) + return Code(code, scope), position + + +def _get_regex(data, position, dummy0, dummy1): + """Decode a BSON regex to bson.regex.Regex or a python pattern object.""" + pattern, position = _get_c_string(data, position) + bson_flags, position = _get_c_string(data, position) + bson_re = Regex(pattern, bson_flags) + return bson_re, position + + +def _get_ref(data, position, obj_end, opts): + """Decode (deprecated) BSON DBPointer to bson.dbref.DBRef.""" + collection, position = _get_string(data, position, obj_end, opts) + oid, position = _get_oid(data, position, obj_end, opts) + return DBRef(collection, oid), position + + +def _get_timestamp(data, position, dummy0, dummy1): + """Decode a BSON timestamp to bson.timestamp.Timestamp.""" + end = position + 8 + inc, timestamp = _UNPACK_TIMESTAMP(data[position:end]) + return Timestamp(timestamp, inc), end + + +def _get_int64(data, position, dummy0, dummy1): + """Decode a BSON int64 to bson.int64.Int64.""" + end = position + 8 + return Int64(_UNPACK_LONG(data[position:end])[0]), end + + +# Each decoder function's signature is: +# - data: bytes +# - position: int, beginning of object in 'data' to decode +# - obj_end: int, end of object to decode in 'data' if variable-length type +# - opts: a CodecOptions +_ELEMENT_GETTER = { + BSONNUM: _get_float, + BSONSTR: _get_string, + BSONOBJ: _get_object, + BSONARR: _get_array, + BSONBIN: _get_binary, + BSONUND: lambda w, x, y, z: (None, x), # Deprecated undefined + BSONOID: _get_oid, + BSONBOO: _get_boolean, + BSONDAT: _get_date, + BSONNUL: lambda w, x, y, z: (None, x), + BSONRGX: _get_regex, + BSONREF: _get_ref, # Deprecated DBPointer + BSONCOD: _get_code, + BSONSYM: _get_string, # Deprecated symbol + BSONCWS: _get_code_w_scope, + BSONINT: _get_int, + BSONTIM: _get_timestamp, + BSONLON: _get_int64, + BSONMIN: lambda w, x, y, z: (MinKey(), x), + BSONMAX: lambda w, x, y, z: (MaxKey(), x)} + + +def _element_to_dict(data, position, obj_end, opts): + """Decode a single key, value pair.""" + element_type = data[position:position + 1] + position += 1 + element_name, position = _get_c_string(data, position) + value, position = _ELEMENT_GETTER[element_type](data, + position, obj_end, opts) + return element_name, value, position + + +def _elements_to_dict(data, position, obj_end, opts): + """Decode a BSON document.""" + result = opts.document_class() + end = obj_end - 1 + while position < end: + (key, value, position) = _element_to_dict(data, position, obj_end, opts) + result[key] = value + return result + + +def _bson_to_dict(data, opts): + """Decode a BSON string to document_class.""" + try: + obj_size = _UNPACK_INT(data[:4])[0] + except struct.error as exc: + raise InvalidBSON(str(exc)) + if obj_size != len(data): + raise InvalidBSON("invalid object size") + if data[obj_size - 1:obj_size] != b"\x00": + raise InvalidBSON("bad eoo") + try: + return _elements_to_dict(data, 4, obj_size - 1, opts) + except InvalidBSON: + raise + except Exception: + # Change exception type to InvalidBSON but preserve traceback. + _, exc_value, exc_tb = sys.exc_info() + reraise(InvalidBSON, exc_value, exc_tb) +if _USE_C: + _bson_to_dict = _cbson._bson_to_dict + + +_PACK_FLOAT = struct.Struct(">> import collections # From Python standard library. + >>> import bson + >>> from bson.codec_options import CodecOptions + >>> data = bson.BSON.encode({'a': 1}) + >>> decoded_doc = bson.BSON.decode(data) + + >>> options = CodecOptions(document_class=collections.OrderedDict) + >>> decoded_doc = bson.BSON.decode(data, codec_options=options) + >>> type(decoded_doc) + + + :Parameters: + - `codec_options` (optional): An instance of + :class:`~bson.codec_options.CodecOptions`. + + .. versionchanged:: 3.0 + Removed `compile_re` option: PyMongo now always represents BSON + regular expressions as :class:`~bson.regex.Regex` objects. Use + :meth:`~bson.regex.Regex.try_compile` to attempt to convert from a + BSON regular expression to a Python regular expression object. + + Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with + `codec_options`. + + .. versionchanged:: 2.7 + Added `compile_re` option. If set to False, PyMongo represented BSON + regular expressions as :class:`~bson.regex.Regex` objects instead of + attempting to compile BSON regular expressions as Python native + regular expressions, thus preventing errors for some incompatible + patterns, see `PYTHON-500`_. + + .. _PYTHON-500: https://jira.mongodb.org/browse/PYTHON-500 + """ + if not isinstance(codec_options, CodecOptions): + raise _CODEC_OPTIONS_TYPE_ERROR + + return _bson_to_dict(self, codec_options) + + +def has_c(): + """Is the C extension installed? + """ + return _USE_C diff --git a/bson/binary.py b/bson/binary.py new file mode 100644 index 0000000..4620a1c --- /dev/null +++ b/bson/binary.py @@ -0,0 +1,236 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from uuid import UUID + +from bson.py3compat import PY3 + +"""Tools for representing BSON binary data. +""" + +BINARY_SUBTYPE = 0 +"""BSON binary subtype for binary data. + +This is the default subtype for binary data. +""" + +FUNCTION_SUBTYPE = 1 +"""BSON binary subtype for functions. +""" + +OLD_BINARY_SUBTYPE = 2 +"""Old BSON binary subtype for binary data. + +This is the old default subtype, the current +default is :data:`BINARY_SUBTYPE`. +""" + +OLD_UUID_SUBTYPE = 3 +"""Old BSON binary subtype for a UUID. + +:class:`uuid.UUID` instances will automatically be encoded +by :mod:`bson` using this subtype. + +.. versionadded:: 2.1 +""" + +UUID_SUBTYPE = 4 +"""BSON binary subtype for a UUID. + +This is the new BSON binary subtype for UUIDs. The +current default is :data:`OLD_UUID_SUBTYPE` but will +change to this in a future release. + +.. versionchanged:: 2.1 + Changed to subtype 4. +""" + +STANDARD = UUID_SUBTYPE +"""The standard UUID representation. + +:class:`uuid.UUID` instances will automatically be encoded to +and decoded from BSON binary, using RFC-4122 byte order with +binary subtype :data:`UUID_SUBTYPE`. + +.. versionadded:: 3.0 +""" + +PYTHON_LEGACY = OLD_UUID_SUBTYPE +"""The Python legacy UUID representation. + +:class:`uuid.UUID` instances will automatically be encoded to +and decoded from BSON binary, using RFC-4122 byte order with +binary subtype :data:`OLD_UUID_SUBTYPE`. + +.. versionadded:: 3.0 +""" + +JAVA_LEGACY = 5 +"""The Java legacy UUID representation. + +:class:`uuid.UUID` instances will automatically be encoded to +and decoded from BSON binary, using the Java driver's legacy +byte order with binary subtype :data:`OLD_UUID_SUBTYPE`. + +.. versionadded:: 2.3 +""" + +CSHARP_LEGACY = 6 +"""The C#/.net legacy UUID representation. + +:class:`uuid.UUID` instances will automatically be encoded to +and decoded from BSON binary, using the C# driver's legacy +byte order and binary subtype :data:`OLD_UUID_SUBTYPE`. + +.. versionadded:: 2.3 +""" + +ALL_UUID_SUBTYPES = (OLD_UUID_SUBTYPE, UUID_SUBTYPE) +ALL_UUID_REPRESENTATIONS = (STANDARD, PYTHON_LEGACY, JAVA_LEGACY, CSHARP_LEGACY) +UUID_REPRESENTATION_NAMES = { + PYTHON_LEGACY: 'PYTHON_LEGACY', + STANDARD: 'STANDARD', + JAVA_LEGACY: 'JAVA_LEGACY', + CSHARP_LEGACY: 'CSHARP_LEGACY'} + +MD5_SUBTYPE = 5 +"""BSON binary subtype for an MD5 hash. +""" + +USER_DEFINED_SUBTYPE = 128 +"""BSON binary subtype for any user defined structure. +""" + + +class Binary(bytes): + """Representation of BSON binary data. + + This is necessary because we want to represent Python strings as + the BSON string type. We need to wrap binary data so we can tell + the difference between what should be considered binary data and + what should be considered a string when we encode to BSON. + + Raises TypeError if `data` is not an instance of :class:`str` + (:class:`bytes` in python 3) or `subtype` is not an instance of + :class:`int`. Raises ValueError if `subtype` is not in [0, 256). + + .. note:: + In python 3 instances of Binary with subtype 0 will be decoded + directly to :class:`bytes`. + + :Parameters: + - `data`: the binary data to represent + - `subtype` (optional): the `binary subtype + `_ + to use + """ + + _type_marker = 5 + + def __new__(cls, data, subtype=BINARY_SUBTYPE): + if not isinstance(data, bytes): + raise TypeError("data must be an instance of bytes") + if not isinstance(subtype, int): + raise TypeError("subtype must be an instance of int") + if subtype >= 256 or subtype < 0: + raise ValueError("subtype must be contained in [0, 256)") + self = bytes.__new__(cls, data) + self.__subtype = subtype + return self + + @property + def subtype(self): + """Subtype of this binary data. + """ + return self.__subtype + + def __getnewargs__(self): + # Work around http://bugs.python.org/issue7382 + data = super(Binary, self).__getnewargs__()[0] + if PY3 and not isinstance(data, bytes): + data = data.encode('latin-1') + return data, self.__subtype + + def __eq__(self, other): + if isinstance(other, Binary): + return ((self.__subtype, bytes(self)) == + (other.subtype, bytes(other))) + # We don't return NotImplemented here because if we did then + # Binary("foo") == "foo" would return True, since Binary is a + # subclass of str... + return False + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return "Binary(%s, %s)" % (bytes.__repr__(self), self.__subtype) + + +class UUIDLegacy(Binary): + """UUID wrapper to support working with UUIDs stored as PYTHON_LEGACY. + + .. doctest:: + + >>> import uuid + >>> from bson.binary import Binary, UUIDLegacy, STANDARD + >>> from bson.codec_options import CodecOptions + >>> my_uuid = uuid.uuid4() + >>> coll = db.get_collection('test', + ... CodecOptions(uuid_representation=STANDARD)) + >>> coll.insert_one({'uuid': Binary(my_uuid.bytes, 3)}).inserted_id + ObjectId('...') + >>> coll.find({'uuid': my_uuid}).count() + 0 + >>> coll.find({'uuid': UUIDLegacy(my_uuid)}).count() + 1 + >>> coll.find({'uuid': UUIDLegacy(my_uuid)})[0]['uuid'] + UUID('...') + >>> + >>> # Convert from subtype 3 to subtype 4 + >>> doc = coll.find_one({'uuid': UUIDLegacy(my_uuid)}) + >>> coll.replace_one({"_id": doc["_id"]}, doc).matched_count + 1 + >>> coll.find({'uuid': UUIDLegacy(my_uuid)}).count() + 0 + >>> coll.find({'uuid': {'$in': [UUIDLegacy(my_uuid), my_uuid]}}).count() + 1 + >>> coll.find_one({'uuid': my_uuid})['uuid'] + UUID('...') + + Raises TypeError if `obj` is not an instance of :class:`~uuid.UUID`. + + :Parameters: + - `obj`: An instance of :class:`~uuid.UUID`. + """ + + def __new__(cls, obj): + if not isinstance(obj, UUID): + raise TypeError("obj must be an instance of uuid.UUID") + self = Binary.__new__(cls, obj.bytes, OLD_UUID_SUBTYPE) + self.__uuid = obj + return self + + def __getnewargs__(self): + # Support copy and deepcopy + return (self.__uuid,) + + @property + def uuid(self): + """UUID instance wrapped by this UUIDLegacy instance. + """ + return self.__uuid + + def __repr__(self): + return "UUIDLegacy('%s')" % self.__uuid diff --git a/bson/code.py b/bson/code.py new file mode 100644 index 0000000..3a5c6b5 --- /dev/null +++ b/bson/code.py @@ -0,0 +1,81 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for representing JavaScript code in BSON. +""" +import collections + +from bson.py3compat import string_type + + +class Code(str): + """BSON's JavaScript code type. + + Raises :class:`TypeError` if `code` is not an instance of + :class:`basestring` (:class:`str` in python 3) or `scope` + is not ``None`` or an instance of :class:`dict`. + + Scope variables can be set by passing a dictionary as the `scope` + argument or by using keyword arguments. If a variable is set as a + keyword argument it will override any setting for that variable in + the `scope` dictionary. + + :Parameters: + - `code`: string containing JavaScript code to be evaluated + - `scope` (optional): dictionary representing the scope in which + `code` should be evaluated - a mapping from identifiers (as + strings) to values + - `**kwargs` (optional): scope variables can also be passed as + keyword arguments + """ + + _type_marker = 13 + + def __new__(cls, code, scope=None, **kwargs): + if not isinstance(code, string_type): + raise TypeError("code must be an " + "instance of %s" % (string_type.__name__)) + + self = str.__new__(cls, code) + + try: + self.__scope = code.scope + except AttributeError: + self.__scope = {} + + if scope is not None: + if not isinstance(scope, collections.Mapping): + raise TypeError("scope must be an instance of dict") + self.__scope.update(scope) + + self.__scope.update(kwargs) + + return self + + @property + def scope(self): + """Scope dictionary for this instance. + """ + return self.__scope + + def __repr__(self): + return "Code(%s, %r)" % (str.__repr__(self), self.__scope) + + def __eq__(self, other): + if isinstance(other, Code): + return (self.__scope, str(self)) == (other.__scope, str(other)) + return False + + def __ne__(self, other): + return not self == other diff --git a/bson/codec_options.py b/bson/codec_options.py new file mode 100644 index 0000000..002f3f4 --- /dev/null +++ b/bson/codec_options.py @@ -0,0 +1,81 @@ +# Copyright 2014-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for specifying BSON codec options.""" + +from collections import MutableMapping, namedtuple + +from bson.binary import (ALL_UUID_REPRESENTATIONS, + PYTHON_LEGACY, + UUID_REPRESENTATION_NAMES) + + +_options_base = namedtuple( + 'CodecOptions', ('document_class', 'tz_aware', 'uuid_representation')) + + +class CodecOptions(_options_base): + """Encapsulates BSON options used in CRUD operations. + + :Parameters: + - `document_class`: BSON documents returned in queries will be decoded + to an instance of this class. Must be a subclass of + :class:`~collections.MutableMapping`. Defaults to :class:`dict`. + - `tz_aware`: If ``True``, BSON datetimes will be decoded to timezone + aware instances of :class:`~datetime.datetime`. Otherwise they will be + naive. Defaults to ``False``. + - `uuid_representation`: The BSON representation to use when encoding + and decoding instances of :class:`~uuid.UUID`. Defaults to + :data:`~bson.binary.PYTHON_LEGACY`. + """ + + def __new__(cls, document_class=dict, + tz_aware=False, uuid_representation=PYTHON_LEGACY): + if not issubclass(document_class, MutableMapping): + raise TypeError("document_class must be dict, bson.son.SON, or " + "another subclass of collections.MutableMapping") + if not isinstance(tz_aware, bool): + raise TypeError("tz_aware must be True or False") + if uuid_representation not in ALL_UUID_REPRESENTATIONS: + raise ValueError("uuid_representation must be a value " + "from bson.binary.ALL_UUID_REPRESENTATIONS") + + return tuple.__new__( + cls, (document_class, tz_aware, uuid_representation)) + + def __repr__(self): + document_class_repr = ( + 'dict' if self.document_class is dict + else repr(self.document_class)) + + uuid_rep_repr = UUID_REPRESENTATION_NAMES.get(self.uuid_representation, + self.uuid_representation) + + return ( + 'CodecOptions(document_class=%s, tz_aware=%r, uuid_representation=' + '%s)' % (document_class_repr, self.tz_aware, uuid_rep_repr)) + + +DEFAULT_CODEC_OPTIONS = CodecOptions() + + +def _parse_codec_options(options): + """Parse BSON codec options.""" + return CodecOptions( + document_class=options.get( + 'document_class', DEFAULT_CODEC_OPTIONS.document_class), + tz_aware=options.get( + 'tz_aware', DEFAULT_CODEC_OPTIONS.tz_aware), + uuid_representation=options.get( + 'uuidrepresentation', DEFAULT_CODEC_OPTIONS.uuid_representation)) diff --git a/bson/dbref.py b/bson/dbref.py new file mode 100644 index 0000000..3ec5463 --- /dev/null +++ b/bson/dbref.py @@ -0,0 +1,135 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for manipulating DBRefs (references to MongoDB documents).""" + +from copy import deepcopy + +from bson.py3compat import iteritems, string_type +from bson.son import SON + + +class DBRef(object): + """A reference to a document stored in MongoDB. + """ + + # DBRef isn't actually a BSON "type" so this number was arbitrarily chosen. + _type_marker = 100 + + def __init__(self, collection, id, database=None, _extra={}, **kwargs): + """Initialize a new :class:`DBRef`. + + Raises :class:`TypeError` if `collection` or `database` is not + an instance of :class:`basestring` (:class:`str` in python 3). + `database` is optional and allows references to documents to work + across databases. Any additional keyword arguments will create + additional fields in the resultant embedded document. + + :Parameters: + - `collection`: name of the collection the document is stored in + - `id`: the value of the document's ``"_id"`` field + - `database` (optional): name of the database to reference + - `**kwargs` (optional): additional keyword arguments will + create additional, custom fields + + .. mongodoc:: dbrefs + """ + if not isinstance(collection, string_type): + raise TypeError("collection must be an " + "instance of %s" % string_type.__name__) + if database is not None and not isinstance(database, string_type): + raise TypeError("database must be an " + "instance of %s" % string_type.__name__) + + self.__collection = collection + self.__id = id + self.__database = database + kwargs.update(_extra) + self.__kwargs = kwargs + + @property + def collection(self): + """Get the name of this DBRef's collection as unicode. + """ + return self.__collection + + @property + def id(self): + """Get this DBRef's _id. + """ + return self.__id + + @property + def database(self): + """Get the name of this DBRef's database. + + Returns None if this DBRef doesn't specify a database. + """ + return self.__database + + def __getattr__(self, key): + try: + return self.__kwargs[key] + except KeyError: + raise AttributeError(key) + + # Have to provide __setstate__ to avoid + # infinite recursion since we override + # __getattr__. + def __setstate__(self, state): + self.__dict__.update(state) + + def as_doc(self): + """Get the SON document representation of this DBRef. + + Generally not needed by application developers + """ + doc = SON([("$ref", self.collection), + ("$id", self.id)]) + if self.database is not None: + doc["$db"] = self.database + doc.update(self.__kwargs) + return doc + + def __repr__(self): + extra = "".join([", %s=%r" % (k, v) + for k, v in iteritems(self.__kwargs)]) + if self.database is None: + return "DBRef(%r, %r%s)" % (self.collection, self.id, extra) + return "DBRef(%r, %r, %r%s)" % (self.collection, self.id, + self.database, extra) + + def __eq__(self, other): + if isinstance(other, DBRef): + us = (self.__database, self.__collection, + self.__id, self.__kwargs) + them = (other.__database, other.__collection, + other.__id, other.__kwargs) + return us == them + return NotImplemented + + def __ne__(self, other): + return not self == other + + def __hash__(self): + """Get a hash value for this :class:`DBRef`.""" + return hash((self.__collection, self.__id, self.__database, + tuple(sorted(self.__kwargs.items())))) + + def __deepcopy__(self, memo): + """Support function for `copy.deepcopy()`.""" + return DBRef(deepcopy(self.__collection, memo), + deepcopy(self.__id, memo), + deepcopy(self.__database, memo), + deepcopy(self.__kwargs, memo)) diff --git a/bson/errors.py b/bson/errors.py new file mode 100644 index 0000000..b6c3864 --- /dev/null +++ b/bson/errors.py @@ -0,0 +1,40 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Exceptions raised by the BSON package.""" + + +class BSONError(Exception): + """Base class for all BSON exceptions. + """ + + +class InvalidBSON(BSONError): + """Raised when trying to create a BSON object from invalid data. + """ + + +class InvalidStringData(BSONError): + """Raised when trying to encode a string containing non-UTF8 data. + """ + + +class InvalidDocument(BSONError): + """Raised when trying to create a BSON object from an invalid document. + """ + + +class InvalidId(BSONError): + """Raised when trying to create an ObjectId from invalid data. + """ diff --git a/bson/int64.py b/bson/int64.py new file mode 100644 index 0000000..77e9812 --- /dev/null +++ b/bson/int64.py @@ -0,0 +1,34 @@ +# Copyright 2014-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A BSON wrapper for long (int in python3)""" + +from bson.py3compat import PY3 + +if PY3: + long = int + + +class Int64(long): + """Representation of the BSON int64 type. + + This is necessary because every integral number is an :class:`int` in + Python 3. Small integral numbers are encoded to BSON int32 by default, + but Int64 numbers will always be encoded to BSON int64. + + :Parameters: + - `value`: the numeric value to represent + """ + + _type_marker = 18 diff --git a/bson/json_util.py b/bson/json_util.py new file mode 100644 index 0000000..cbbff7c --- /dev/null +++ b/bson/json_util.py @@ -0,0 +1,257 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for using Python's :mod:`json` module with BSON documents. + +This module provides two helper methods `dumps` and `loads` that wrap the +native :mod:`json` methods and provide explicit BSON conversion to and from +json. This allows for specialized encoding and decoding of BSON documents +into `Mongo Extended JSON +`_'s *Strict* +mode. This lets you encode / decode BSON documents to JSON even when +they use special BSON types. + +Example usage (serialization): + +.. doctest:: + + >>> from bson import Binary, Code + >>> from bson.json_util import dumps + >>> dumps([{'foo': [1, 2]}, + ... {'bar': {'hello': 'world'}}, + ... {'code': Code("function x() { return 1; }")}, + ... {'bin': Binary("\x01\x02\x03\x04")}]) + '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]' + +Example usage (deserialization): + +.. doctest:: + + >>> from bson.json_util import loads + >>> loads('[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": "00", "$binary": "AQIDBA=="}}]') + [{u'foo': [1, 2]}, {u'bar': {u'hello': u'world'}}, {u'code': Code('function x() { return 1; }', {})}, {u'bin': Binary('...', 0)}] + +Alternatively, you can manually pass the `default` to :func:`json.dumps`. +It won't handle :class:`~bson.binary.Binary` and :class:`~bson.code.Code` +instances (as they are extended strings you can't provide custom defaults), +but it will be faster as there is less recursion. + +.. versionchanged:: 2.8 + The output format for :class:`~bson.timestamp.Timestamp` has changed from + '{"t": , "i": }' to '{"$timestamp": {"t": , "i": }}'. + This new format will be decoded to an instance of + :class:`~bson.timestamp.Timestamp`. The old format will continue to be + decoded to a python dict as before. Encoding to the old format is no longer + supported as it was never correct and loses type information. + Added support for $numberLong and $undefined - new in MongoDB 2.6 - and + parsing $date in ISO-8601 format. + +.. versionchanged:: 2.7 + Preserves order when rendering SON, Timestamp, Code, Binary, and DBRef + instances. + +.. versionchanged:: 2.3 + Added dumps and loads helpers to automatically handle conversion to and + from json and supports :class:`~bson.binary.Binary` and + :class:`~bson.code.Code` +""" + +import base64 +import calendar +import collections +import datetime +import json +import re +import uuid + +from bson import EPOCH_AWARE, RE_TYPE, SON +from bson.binary import Binary +from bson.code import Code +from bson.dbref import DBRef +from bson.int64 import Int64 +from bson.max_key import MaxKey +from bson.min_key import MinKey +from bson.objectid import ObjectId +from bson.regex import Regex +from bson.timestamp import Timestamp +from bson.tz_util import utc + +from bson.py3compat import PY3, iteritems, string_type, text_type + + +_RE_OPT_TABLE = { + "i": re.I, + "l": re.L, + "m": re.M, + "s": re.S, + "u": re.U, + "x": re.X, +} + + +def dumps(obj, *args, **kwargs): + """Helper function that wraps :class:`json.dumps`. + + Recursive function that handles all BSON types including + :class:`~bson.binary.Binary` and :class:`~bson.code.Code`. + + .. versionchanged:: 2.7 + Preserves order when rendering SON, Timestamp, Code, Binary, and DBRef + instances. + """ + return json.dumps(_json_convert(obj), *args, **kwargs) + + +def loads(s, *args, **kwargs): + """Helper function that wraps :class:`json.loads`. + + Automatically passes the object_hook for BSON type conversion. + """ + kwargs['object_hook'] = lambda dct: object_hook(dct) + return json.loads(s, *args, **kwargs) + + +def _json_convert(obj): + """Recursive helper method that converts BSON types so they can be + converted into json. + """ + if hasattr(obj, 'iteritems') or hasattr(obj, 'items'): # PY3 support + return SON(((k, _json_convert(v)) for k, v in iteritems(obj))) + elif hasattr(obj, '__iter__') and not isinstance(obj, (text_type, bytes)): + return list((_json_convert(v) for v in obj)) + try: + return default(obj) + except TypeError: + return obj + + +def object_hook(dct): + if "$oid" in dct: + return ObjectId(str(dct["$oid"])) + if "$ref" in dct: + return DBRef(dct["$ref"], dct["$id"], dct.get("$db", None)) + if "$date" in dct: + dtm = dct["$date"] + # mongoexport 2.6 and newer + if isinstance(dtm, string_type): + aware = datetime.datetime.strptime( + dtm[:23], "%Y-%m-%dT%H:%M:%S.%f").replace(tzinfo=utc) + offset = dtm[23:] + if not offset or offset == 'Z': + # UTC + return aware + else: + if len(offset) == 5: + # Offset from mongoexport is in format (+|-)HHMM + secs = (int(offset[1:3]) * 3600 + int(offset[3:]) * 60) + elif ':' in offset and len(offset) == 6: + # RFC-3339 format (+|-)HH:MM + hours, minutes = offset[1:].split(':') + secs = (int(hours) * 3600 + int(minutes) * 60) + else: + # Not RFC-3339 compliant or mongoexport output. + raise ValueError("invalid format for offset") + if offset[0] == "-": + secs *= -1 + return aware - datetime.timedelta(seconds=secs) + # mongoexport 2.6 and newer, time before the epoch (SERVER-15275) + elif isinstance(dtm, collections.Mapping): + secs = float(dtm["$numberLong"]) / 1000.0 + # mongoexport before 2.6 + else: + secs = float(dtm) / 1000.0 + return EPOCH_AWARE + datetime.timedelta(seconds=secs) + if "$regex" in dct: + flags = 0 + # PyMongo always adds $options but some other tools may not. + for opt in dct.get("$options", ""): + flags |= _RE_OPT_TABLE.get(opt, 0) + return Regex(dct["$regex"], flags) + if "$minKey" in dct: + return MinKey() + if "$maxKey" in dct: + return MaxKey() + if "$binary" in dct: + if isinstance(dct["$type"], int): + dct["$type"] = "%02x" % dct["$type"] + subtype = int(dct["$type"], 16) + if subtype >= 0xffffff80: # Handle mongoexport values + subtype = int(dct["$type"][6:], 16) + return Binary(base64.b64decode(dct["$binary"].encode()), subtype) + if "$code" in dct: + return Code(dct["$code"], dct.get("$scope")) + if "$uuid" in dct: + return uuid.UUID(dct["$uuid"]) + if "$undefined" in dct: + return None + if "$numberLong" in dct: + return Int64(dct["$numberLong"]) + if "$timestamp" in dct: + tsp = dct["$timestamp"] + return Timestamp(tsp["t"], tsp["i"]) + return dct + + +def default(obj): + # We preserve key order when rendering SON, DBRef, etc. as JSON by + # returning a SON for those types instead of a dict. + if isinstance(obj, ObjectId): + return {"$oid": str(obj)} + if isinstance(obj, DBRef): + return _json_convert(obj.as_doc()) + if isinstance(obj, datetime.datetime): + # TODO share this code w/ bson.py? + if obj.utcoffset() is not None: + obj = obj - obj.utcoffset() + millis = int(calendar.timegm(obj.timetuple()) * 1000 + + obj.microsecond / 1000) + return {"$date": millis} + if isinstance(obj, (RE_TYPE, Regex)): + flags = "" + if obj.flags & re.IGNORECASE: + flags += "i" + if obj.flags & re.LOCALE: + flags += "l" + if obj.flags & re.MULTILINE: + flags += "m" + if obj.flags & re.DOTALL: + flags += "s" + if obj.flags & re.UNICODE: + flags += "u" + if obj.flags & re.VERBOSE: + flags += "x" + if isinstance(obj.pattern, text_type): + pattern = obj.pattern + else: + pattern = obj.pattern.decode('utf-8') + return SON([("$regex", pattern), ("$options", flags)]) + if isinstance(obj, MinKey): + return {"$minKey": 1} + if isinstance(obj, MaxKey): + return {"$maxKey": 1} + if isinstance(obj, Timestamp): + return {"$timestamp": SON([("t", obj.time), ("i", obj.inc)])} + if isinstance(obj, Code): + return SON([('$code', str(obj)), ('$scope', obj.scope)]) + if isinstance(obj, Binary): + return SON([ + ('$binary', base64.b64encode(obj).decode()), + ('$type', "%02x" % obj.subtype)]) + if PY3 and isinstance(obj, bytes): + return SON([ + ('$binary', base64.b64encode(obj).decode()), + ('$type', "00")]) + if isinstance(obj, uuid.UUID): + return {"$uuid": obj.hex} + raise TypeError("%r is not JSON serializable" % obj) diff --git a/bson/max_key.py b/bson/max_key.py new file mode 100644 index 0000000..9ed9ab5 --- /dev/null +++ b/bson/max_key.py @@ -0,0 +1,47 @@ +# Copyright 2010-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Representation for the MongoDB internal MaxKey type. +""" + + +class MaxKey(object): + """MongoDB internal MaxKey type. + + .. versionchanged:: 2.7 + ``MaxKey`` now implements comparison operators. + """ + + _type_marker = 127 + + def __eq__(self, other): + return isinstance(other, MaxKey) + + def __ne__(self, other): + return not self == other + + def __le__(self, other): + return isinstance(other, MaxKey) + + def __lt__(self, dummy): + return False + + def __ge__(self, dummy): + return True + + def __gt__(self, other): + return not isinstance(other, MaxKey) + + def __repr__(self): + return "MaxKey()" diff --git a/bson/min_key.py b/bson/min_key.py new file mode 100644 index 0000000..ee135af --- /dev/null +++ b/bson/min_key.py @@ -0,0 +1,47 @@ +# Copyright 2010-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Representation for the MongoDB internal MinKey type. +""" + + +class MinKey(object): + """MongoDB internal MinKey type. + + .. versionchanged:: 2.7 + ``MinKey`` now implements comparison operators. + """ + + _type_marker = 255 + + def __eq__(self, other): + return isinstance(other, MinKey) + + def __ne__(self, other): + return not self == other + + def __le__(self, dummy): + return True + + def __lt__(self, other): + return not isinstance(other, MinKey) + + def __ge__(self, other): + return isinstance(other, MinKey) + + def __gt__(self, dummy): + return False + + def __repr__(self): + return "MinKey()" diff --git a/bson/objectid.py b/bson/objectid.py new file mode 100644 index 0000000..42b6109 --- /dev/null +++ b/bson/objectid.py @@ -0,0 +1,292 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for working with MongoDB `ObjectIds +`_. +""" + +import binascii +import calendar +import datetime +import hashlib +import os +import random +import socket +import struct +import threading +import time + +from bson.errors import InvalidId +from bson.py3compat import PY3, bytes_from_hex, string_type, text_type +from bson.tz_util import utc + + +def _machine_bytes(): + """Get the machine portion of an ObjectId. + """ + machine_hash = hashlib.md5() + if PY3: + # gethostname() returns a unicode string in python 3.x + # while update() requires a byte string. + machine_hash.update(socket.gethostname().encode()) + else: + # Calling encode() here will fail with non-ascii hostnames + machine_hash.update(socket.gethostname()) + return machine_hash.digest()[0:3] + + +def _raise_invalid_id(oid): + raise InvalidId( + "%r is not a valid ObjectId, it must be a 12-byte input" + " or a 24-character hex string" % oid) + + +class ObjectId(object): + """A MongoDB ObjectId. + """ + + _inc = random.randint(0, 0xFFFFFF) + _inc_lock = threading.Lock() + + _machine_bytes = _machine_bytes() + + __slots__ = ('__id') + + _type_marker = 7 + + def __init__(self, oid=None): + """Initialize a new ObjectId. + + An ObjectId is a 12-byte unique identifier consisting of: + + - a 4-byte value representing the seconds since the Unix epoch, + - a 3-byte machine identifier, + - a 2-byte process id, and + - a 3-byte counter, starting with a random value. + + By default, ``ObjectId()`` creates a new unique identifier. The + optional parameter `oid` can be an :class:`ObjectId`, or any 12 + :class:`bytes` or, in Python 2, any 12-character :class:`str`. + + For example, the 12 bytes b'foo-bar-quux' do not follow the ObjectId + specification but they are acceptable input:: + + >>> ObjectId(b'foo-bar-quux') + ObjectId('666f6f2d6261722d71757578') + + `oid` can also be a :class:`unicode` or :class:`str` of 24 hex digits:: + + >>> ObjectId('0123456789ab0123456789ab') + ObjectId('0123456789ab0123456789ab') + >>> + >>> # A u-prefixed unicode literal: + >>> ObjectId(u'0123456789ab0123456789ab') + ObjectId('0123456789ab0123456789ab') + + Raises :class:`~bson.errors.InvalidId` if `oid` is not 12 bytes nor + 24 hex digits, or :class:`TypeError` if `oid` is not an accepted type. + + :Parameters: + - `oid` (optional): a valid ObjectId. + + .. mongodoc:: objectids + """ + if oid is None: + self.__generate() + elif isinstance(oid, bytes) and len(oid) == 12: + self.__id = oid + else: + self.__validate(oid) + + @classmethod + def from_datetime(cls, generation_time): + """Create a dummy ObjectId instance with a specific generation time. + + This method is useful for doing range queries on a field + containing :class:`ObjectId` instances. + + .. warning:: + It is not safe to insert a document containing an ObjectId + generated using this method. This method deliberately + eliminates the uniqueness guarantee that ObjectIds + generally provide. ObjectIds generated with this method + should be used exclusively in queries. + + `generation_time` will be converted to UTC. Naive datetime + instances will be treated as though they already contain UTC. + + An example using this helper to get documents where ``"_id"`` + was generated before January 1, 2010 would be: + + >>> gen_time = datetime.datetime(2010, 1, 1) + >>> dummy_id = ObjectId.from_datetime(gen_time) + >>> result = collection.find({"_id": {"$lt": dummy_id}}) + + :Parameters: + - `generation_time`: :class:`~datetime.datetime` to be used + as the generation time for the resulting ObjectId. + """ + if generation_time.utcoffset() is not None: + generation_time = generation_time - generation_time.utcoffset() + timestamp = calendar.timegm(generation_time.timetuple()) + oid = struct.pack( + ">i", int(timestamp)) + b"\x00\x00\x00\x00\x00\x00\x00\x00" + return cls(oid) + + @classmethod + def is_valid(cls, oid): + """Checks if a `oid` string is valid or not. + + :Parameters: + - `oid`: the object id to validate + + .. versionadded:: 2.3 + """ + if not oid: + return False + + try: + ObjectId(oid) + return True + except (InvalidId, TypeError): + return False + + def __generate(self): + """Generate a new value for this ObjectId. + """ + + # 4 bytes current time + oid = struct.pack(">i", int(time.time())) + + # 3 bytes machine + oid += ObjectId._machine_bytes + + # 2 bytes pid + oid += struct.pack(">H", os.getpid() % 0xFFFF) + + # 3 bytes inc + with ObjectId._inc_lock: + oid += struct.pack(">i", ObjectId._inc)[1:4] + ObjectId._inc = (ObjectId._inc + 1) % 0xFFFFFF + + self.__id = oid + + def __validate(self, oid): + """Validate and use the given id for this ObjectId. + + Raises TypeError if id is not an instance of + (:class:`basestring` (:class:`str` or :class:`bytes` + in python 3), ObjectId) and InvalidId if it is not a + valid ObjectId. + + :Parameters: + - `oid`: a valid ObjectId + """ + if isinstance(oid, ObjectId): + self.__id = oid.binary + # bytes or unicode in python 2, str in python 3 + elif isinstance(oid, string_type): + if len(oid) == 24: + try: + self.__id = bytes_from_hex(oid) + except (TypeError, ValueError): + _raise_invalid_id(oid) + else: + _raise_invalid_id(oid) + else: + raise TypeError("id must be an instance of (bytes, %s, ObjectId), " + "not %s" % (text_type.__name__, type(oid))) + + @property + def binary(self): + """12-byte binary representation of this ObjectId. + """ + return self.__id + + @property + def generation_time(self): + """A :class:`datetime.datetime` instance representing the time of + generation for this :class:`ObjectId`. + + The :class:`datetime.datetime` is timezone aware, and + represents the generation time in UTC. It is precise to the + second. + """ + timestamp = struct.unpack(">i", self.__id[0:4])[0] + return datetime.datetime.fromtimestamp(timestamp, utc) + + def __getstate__(self): + """return value of object for pickling. + needed explicitly because __slots__() defined. + """ + return self.__id + + def __setstate__(self, value): + """explicit state set from pickling + """ + # Provide backwards compatability with OIDs + # pickled with pymongo-1.9 or older. + if isinstance(value, dict): + oid = value["_ObjectId__id"] + else: + oid = value + # ObjectIds pickled in python 2.x used `str` for __id. + # In python 3.x this has to be converted to `bytes` + # by encoding latin-1. + if PY3 and isinstance(oid, text_type): + self.__id = oid.encode('latin-1') + else: + self.__id = oid + + def __str__(self): + if PY3: + return binascii.hexlify(self.__id).decode() + return binascii.hexlify(self.__id) + + def __repr__(self): + return "ObjectId('%s')" % (str(self),) + + def __eq__(self, other): + if isinstance(other, ObjectId): + return self.__id == other.binary + return NotImplemented + + def __ne__(self, other): + if isinstance(other, ObjectId): + return self.__id != other.binary + return NotImplemented + + def __lt__(self, other): + if isinstance(other, ObjectId): + return self.__id < other.binary + return NotImplemented + + def __le__(self, other): + if isinstance(other, ObjectId): + return self.__id <= other.binary + return NotImplemented + + def __gt__(self, other): + if isinstance(other, ObjectId): + return self.__id > other.binary + return NotImplemented + + def __ge__(self, other): + if isinstance(other, ObjectId): + return self.__id >= other.binary + return NotImplemented + + def __hash__(self): + """Get a hash value for this :class:`ObjectId`.""" + return hash(self.__id) diff --git a/bson/py3compat.py b/bson/py3compat.py new file mode 100644 index 0000000..b300b14 --- /dev/null +++ b/bson/py3compat.py @@ -0,0 +1,96 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you +# may not use this file except in compliance with the License. You +# may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""Utility functions and definitions for python3 compatibility.""" + +import sys + +PY3 = sys.version_info[0] == 3 + +if PY3: + import codecs + import _thread as thread + from io import BytesIO as StringIO + MAXSIZE = sys.maxsize + + imap = map + + def b(s): + # BSON and socket operations deal in binary data. In + # python 3 that means instances of `bytes`. In python + # 2.6 and 2.7 you can create an alias for `bytes` using + # the b prefix (e.g. b'foo'). + # See http://python3porting.com/problems.html#nicer-solutions + return codecs.latin_1_encode(s)[0] + + def u(s): + # PY3 strings may already be treated as unicode literals + return s + + def bytes_from_hex(h): + return bytes.fromhex(h) + + def iteritems(d): + return iter(d.items()) + + def itervalues(d): + return iter(d.values()) + + def reraise(exctype, value, trace=None): + raise exctype(str(value)).with_traceback(trace) + + def _unicode(s): + return s + + text_type = str + string_type = str + integer_types = int +else: + import thread + + from itertools import imap + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO + + MAXSIZE = sys.maxint + + def b(s): + # See comments above. In python 2.x b('foo') is just 'foo'. + return s + + def u(s): + """Replacement for unicode literal prefix.""" + return unicode(s.replace('\\', '\\\\'), 'unicode_escape') + + def bytes_from_hex(h): + return h.decode('hex') + + def iteritems(d): + return d.iteritems() + + def itervalues(d): + return d.itervalues() + + # "raise x, y, z" raises SyntaxError in Python 3 + exec("""def reraise(exctype, value, trace=None): + raise exctype, str(value), trace +""") + + _unicode = unicode + + string_type = basestring + text_type = unicode + integer_types = (int, long) diff --git a/bson/regex.py b/bson/regex.py new file mode 100644 index 0000000..3754894 --- /dev/null +++ b/bson/regex.py @@ -0,0 +1,126 @@ +# Copyright 2013-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for representing MongoDB regular expressions. +""" + +import re + +from bson.son import RE_TYPE +from bson.py3compat import string_type, text_type + + +def str_flags_to_int(str_flags): + flags = 0 + if "i" in str_flags: + flags |= re.IGNORECASE + if "l" in str_flags: + flags |= re.LOCALE + if "m" in str_flags: + flags |= re.MULTILINE + if "s" in str_flags: + flags |= re.DOTALL + if "u" in str_flags: + flags |= re.UNICODE + if "x" in str_flags: + flags |= re.VERBOSE + + return flags + + +class Regex(object): + """BSON regular expression data.""" + _type_marker = 11 + + @classmethod + def from_native(cls, regex): + """Convert a Python regular expression into a ``Regex`` instance. + + Note that in Python 3, a regular expression compiled from a + :class:`str` has the ``re.UNICODE`` flag set. If it is undesirable + to store this flag in a BSON regular expression, unset it first:: + + >>> pattern = re.compile('.*') + >>> regex = Regex.from_native(pattern) + >>> regex.flags ^= re.UNICODE + >>> db.collection.insert({'pattern': regex}) + + :Parameters: + - `regex`: A regular expression object from ``re.compile()``. + + .. warning:: + Python regular expressions use a different syntax and different + set of flags than MongoDB, which uses `PCRE`_. A regular + expression retrieved from the server may not compile in + Python, or may match a different set of strings in Python than + when used in a MongoDB query. + + .. _PCRE: http://www.pcre.org/ + """ + if not isinstance(regex, RE_TYPE): + raise TypeError( + "regex must be a compiled regular expression, not %s" + % type(regex)) + + return Regex(regex.pattern, regex.flags) + + def __init__(self, pattern, flags=0): + """BSON regular expression data. + + This class is useful to store and retrieve regular expressions that are + incompatible with Python's regular expression dialect. + + :Parameters: + - `pattern`: string + - `flags`: (optional) an integer bitmask, or a string of flag + characters like "im" for IGNORECASE and MULTILINE + """ + if not isinstance(pattern, (text_type, bytes)): + raise TypeError("pattern must be a string, not %s" % type(pattern)) + self.pattern = pattern + + if isinstance(flags, string_type): + self.flags = str_flags_to_int(flags) + elif isinstance(flags, int): + self.flags = flags + else: + raise TypeError( + "flags must be a string or int, not %s" % type(flags)) + + def __eq__(self, other): + if isinstance(other, Regex): + return self.pattern == self.pattern and self.flags == other.flags + else: + return NotImplemented + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return "Regex(%r, %r)" % (self.pattern, self.flags) + + def try_compile(self): + """Compile this :class:`Regex` as a Python regular expression. + + .. warning:: + Python regular expressions use a different syntax and different + set of flags than MongoDB, which uses `PCRE`_. A regular + expression retrieved from the server may not compile in + Python, or may match a different set of strings in Python than + when used in a MongoDB query. :meth:`try_compile()` may raise + :exc:`re.error`. + + .. _PCRE: http://www.pcre.org/ + """ + return re.compile(self.pattern, self.flags) diff --git a/bson/son.py b/bson/son.py new file mode 100644 index 0000000..dd5f0f6 --- /dev/null +++ b/bson/son.py @@ -0,0 +1,249 @@ +# Copyright 2009-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for creating and manipulating SON, the Serialized Ocument Notation. + +Regular dictionaries can be used instead of SON objects, but not when the order +of keys is important. A SON object can be used just like a normal Python +dictionary.""" + +import collections +import copy +import re + +from bson.py3compat import iteritems + + +# This sort of sucks, but seems to be as good as it gets... +# This is essentially the same as re._pattern_type +RE_TYPE = type(re.compile("")) + + +class SON(dict): + """SON data. + + A subclass of dict that maintains ordering of keys and provides a + few extra niceties for dealing with SON. SON objects can be + converted to and from BSON. + + The mapping from Python types to BSON types is as follows: + + ======================================= ============= =================== + Python Type BSON Type Supported Direction + ======================================= ============= =================== + None null both + bool boolean both + int [#int]_ int32 / int64 py -> bson + long int64 py -> bson + `bson.int64.Int64` int64 both + float number (real) both + string string py -> bson + unicode string both + list array both + dict / `SON` object both + datetime.datetime [#dt]_ [#dt2]_ date both + `bson.regex.Regex` regex both + compiled re [#re]_ regex py -> bson + `bson.binary.Binary` binary both + `bson.objectid.ObjectId` oid both + `bson.dbref.DBRef` dbref both + None undefined bson -> py + unicode code bson -> py + `bson.code.Code` code py -> bson + unicode symbol bson -> py + bytes (Python 3) [#bytes]_ binary both + ======================================= ============= =================== + + Note that to save binary data it must be wrapped as an instance of + `bson.binary.Binary`. Otherwise it will be saved as a BSON string + and retrieved as unicode. + + .. [#int] A Python int will be saved as a BSON int32 or BSON int64 depending + on its size. A BSON int32 will always decode to a Python int. A BSON + int64 will always decode to a :class:`~bson.int64.Int64`. + .. [#dt] datetime.datetime instances will be rounded to the nearest + millisecond when saved + .. [#dt2] all datetime.datetime instances are treated as *naive*. clients + should always use UTC. + .. [#re] :class:`~bson.regex.Regex` instances and regular expression + objects from ``re.compile()`` are both saved as BSON regular expressions. + BSON regular expressions are decoded as :class:`~bson.regex.Regex` + instances. + .. [#bytes] The bytes type from Python 3.x is encoded as BSON binary with + subtype 0. In Python 3.x it will be decoded back to bytes. In Python 2.x + it will be decoded to an instance of :class:`~bson.binary.Binary` with + subtype 0. + """ + + def __init__(self, data=None, **kwargs): + self.__keys = [] + dict.__init__(self) + self.update(data) + self.update(kwargs) + + def __new__(cls, *args, **kwargs): + instance = super(SON, cls).__new__(cls, *args, **kwargs) + instance.__keys = [] + return instance + + def __repr__(self): + result = [] + for key in self.__keys: + result.append("(%r, %r)" % (key, self[key])) + return "SON([%s])" % ", ".join(result) + + def __setitem__(self, key, value): + if key not in self.__keys: + self.__keys.append(key) + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + self.__keys.remove(key) + dict.__delitem__(self, key) + + def keys(self): + return list(self.__keys) + + def copy(self): + other = SON() + other.update(self) + return other + + # TODO this is all from UserDict.DictMixin. it could probably be made more + # efficient. + # second level definitions support higher levels + def __iter__(self): + for k in self.__keys: + yield k + + def has_key(self, key): + return key in self.__keys + + # third level takes advantage of second level definitions + def iteritems(self): + for k in self: + yield (k, self[k]) + + def iterkeys(self): + return self.__iter__() + + # fourth level uses definitions from lower levels + def itervalues(self): + for _, v in self.iteritems(): + yield v + + def values(self): + return [v for _, v in self.iteritems()] + + def items(self): + return [(key, self[key]) for key in self] + + def clear(self): + self.__keys = [] + super(SON, self).clear() + + def setdefault(self, key, default=None): + try: + return self[key] + except KeyError: + self[key] = default + return default + + def pop(self, key, *args): + if len(args) > 1: + raise TypeError("pop expected at most 2 arguments, got "\ + + repr(1 + len(args))) + try: + value = self[key] + except KeyError: + if args: + return args[0] + raise + del self[key] + return value + + def popitem(self): + try: + k, v = next(self.iteritems()) + except StopIteration: + raise KeyError('container is empty') + del self[k] + return (k, v) + + def update(self, other=None, **kwargs): + # Make progressively weaker assumptions about "other" + if other is None: + pass + elif hasattr(other, 'iteritems'): # iteritems saves memory and lookups + for k, v in other.iteritems(): + self[k] = v + elif hasattr(other, 'keys'): + for k in other.keys(): + self[k] = other[k] + else: + for k, v in other: + self[k] = v + if kwargs: + self.update(kwargs) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def __eq__(self, other): + """Comparison to another SON is order-sensitive while comparison to a + regular dictionary is order-insensitive. + """ + if isinstance(other, SON): + return len(self) == len(other) and self.items() == other.items() + return self.to_dict() == other + + def __ne__(self, other): + return not self == other + + def __len__(self): + return len(self.__keys) + + def to_dict(self): + """Convert a SON document to a normal Python dictionary instance. + + This is trickier than just *dict(...)* because it needs to be + recursive. + """ + + def transform_value(value): + if isinstance(value, list): + return [transform_value(v) for v in value] + elif isinstance(value, collections.Mapping): + return dict([ + (k, transform_value(v)) + for k, v in iteritems(value)]) + else: + return value + + return transform_value(dict(self)) + + def __deepcopy__(self, memo): + out = SON() + val_id = id(self) + if val_id in memo: + return memo.get(val_id) + memo[val_id] = out + for k, v in self.iteritems(): + if not isinstance(v, RE_TYPE): + v = copy.deepcopy(v, memo) + out[k] = v + return out diff --git a/bson/timestamp.py b/bson/timestamp.py new file mode 100644 index 0000000..d00e29d --- /dev/null +++ b/bson/timestamp.py @@ -0,0 +1,116 @@ +# Copyright 2010-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools for representing MongoDB internal Timestamps. +""" + +import calendar +import datetime + +from bson.py3compat import integer_types +from bson.tz_util import utc + +UPPERBOUND = 4294967296 + +class Timestamp(object): + """MongoDB internal timestamps used in the opLog. + """ + + _type_marker = 17 + + def __init__(self, time, inc): + """Create a new :class:`Timestamp`. + + This class is only for use with the MongoDB opLog. If you need + to store a regular timestamp, please use a + :class:`~datetime.datetime`. + + Raises :class:`TypeError` if `time` is not an instance of + :class: `int` or :class:`~datetime.datetime`, or `inc` is not + an instance of :class:`int`. Raises :class:`ValueError` if + `time` or `inc` is not in [0, 2**32). + + :Parameters: + - `time`: time in seconds since epoch UTC, or a naive UTC + :class:`~datetime.datetime`, or an aware + :class:`~datetime.datetime` + - `inc`: the incrementing counter + """ + if isinstance(time, datetime.datetime): + if time.utcoffset() is not None: + time = time - time.utcoffset() + time = int(calendar.timegm(time.timetuple())) + if not isinstance(time, integer_types): + raise TypeError("time must be an instance of int") + if not isinstance(inc, integer_types): + raise TypeError("inc must be an instance of int") + if not 0 <= time < UPPERBOUND: + raise ValueError("time must be contained in [0, 2**32)") + if not 0 <= inc < UPPERBOUND: + raise ValueError("inc must be contained in [0, 2**32)") + + self.__time = time + self.__inc = inc + + @property + def time(self): + """Get the time portion of this :class:`Timestamp`. + """ + return self.__time + + @property + def inc(self): + """Get the inc portion of this :class:`Timestamp`. + """ + return self.__inc + + def __eq__(self, other): + if isinstance(other, Timestamp): + return (self.__time == other.time and self.__inc == other.inc) + else: + return NotImplemented + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if isinstance(other, Timestamp): + return (self.time, self.inc) < (other.time, other.inc) + return NotImplemented + + def __le__(self, other): + if isinstance(other, Timestamp): + return (self.time, self.inc) <= (other.time, other.inc) + return NotImplemented + + def __gt__(self, other): + if isinstance(other, Timestamp): + return (self.time, self.inc) > (other.time, other.inc) + return NotImplemented + + def __ge__(self, other): + if isinstance(other, Timestamp): + return (self.time, self.inc) >= (other.time, other.inc) + return NotImplemented + + def __repr__(self): + return "Timestamp(%s, %s)" % (self.__time, self.__inc) + + def as_datetime(self): + """Return a :class:`~datetime.datetime` instance corresponding + to the time portion of this :class:`Timestamp`. + + The returned datetime's timezone is UTC. + """ + return datetime.datetime.fromtimestamp(self.__time, utc) diff --git a/bson/tz_util.py b/bson/tz_util.py new file mode 100644 index 0000000..6ec918f --- /dev/null +++ b/bson/tz_util.py @@ -0,0 +1,52 @@ +# Copyright 2010-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Timezone related utilities for BSON.""" + +from datetime import (timedelta, + tzinfo) + +ZERO = timedelta(0) + + +class FixedOffset(tzinfo): + """Fixed offset timezone, in minutes east from UTC. + + Implementation based from the Python `standard library documentation + `_. + Defining __getinitargs__ enables pickling / copying. + """ + + def __init__(self, offset, name): + if isinstance(offset, timedelta): + self.__offset = offset + else: + self.__offset = timedelta(minutes=offset) + self.__name = name + + def __getinitargs__(self): + return self.__offset, self.__name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return ZERO + + +utc = FixedOffset(0, "UTC") +"""Fixed offset timezone representing UTC.""" diff --git a/process.py b/process.py new file mode 100644 index 0000000..edc8b75 --- /dev/null +++ b/process.py @@ -0,0 +1,99 @@ +__author__ = 'zephyre' + +from datetime import datetime + +import alfred +from bson import ObjectId +from bson.errors import InvalidId + + +def process(query_str): + """ Entry point """ + query_str = query_str.strip().lower() if query_str else '' + + results = [] + if query_str == 'gen': + ret = gen_objectid_result() + results.append(ret) + + oid = ret.attributes['arg'] + results.extend(get_generation_time(oid)) + else: + oid = parse_query_value(query_str) + if oid: + results.extend(get_generation_time(oid)) + + if results: + xml = alfred.xml(results) # compiles the XML answer + alfred.write(xml) # writes the XML back to Alfred + + +def get_generation_time(oid): + """ + Get the alfred result for the ObjectId generation time + :param oid: + :return: + """ + import pytz + from tzlocal import get_localzone + + gt = oid.generation_time.replace(tzinfo=pytz.UTC) + t0 = pytz.UTC.localize(datetime.utcfromtimestamp(0)) + + tz_cn = pytz.timezone('Asia/Shanghai') + t1 = tz_cn.normalize(gt) + + tz_local = get_localzone() + t2 = tz_local.normalize(gt) + + total_seconds = int((gt - t0).total_seconds()) + + return [ + alfred.Item(title=str(gt), subtitle='Generation time in UTC', icon='', attributes={'arg': gt}), + alfred.Item(title=str(t1), subtitle='Generation time in Asia/Shanghai', icon='', attributes={'arg': t1}), + alfred.Item(title=str(t2), subtitle='Generation time in local timezone: %s' % tz_local.zone, icon='', + attributes={'arg': t1}), + alfred.Item(title=str(total_seconds), subtitle='Unix Epoch Timestamp', icon='', + attributes={'arg': total_seconds}) + ] + + +def gen_objectid_result(): + oid = ObjectId() + + return alfred.Item(title=str(oid), subtitle='Generated ObjectId', icon='', attributes={'arg': oid}) + + +def parse_query_value(query_str): + """ + Parse the query string. If the input is invalid or doesn't exist, None will be returned + :param query_str: + :return: + """ + + try: + oid = ObjectId(query_str) + except InvalidId: + oid = None + + return oid + + +# def alfred_items_for_value(oid): +# index = 0 +# alfred_items = [ +# alfred.Item(title=str(oid), subtitle='Original ObjectId', attributes={ +# 'uid': alfred.uid(index), +# 'arg': oid, +# }, icon='icon.png') +# ] +# return alfred_items + + +if __name__ == '__main__': + try: + arg = alfred.args()[0] + except IndexError: + arg = None + process(arg) + diff --git a/tzlocal/__init__.py b/tzlocal/__init__.py new file mode 100644 index 0000000..df7a66b --- /dev/null +++ b/tzlocal/__init__.py @@ -0,0 +1,7 @@ +import sys +if sys.platform == 'win32': + from tzlocal.win32 import get_localzone, reload_localzone +elif 'darwin' in sys.platform: + from tzlocal.darwin import get_localzone, reload_localzone +else: + from tzlocal.unix import get_localzone, reload_localzone diff --git a/tzlocal/darwin.py b/tzlocal/darwin.py new file mode 100644 index 0000000..86fd906 --- /dev/null +++ b/tzlocal/darwin.py @@ -0,0 +1,27 @@ +from __future__ import with_statement +import os +import pytz + +_cache_tz = None + +def _get_localzone(): + tzname = os.popen("systemsetup -gettimezone").read().replace("Time Zone: ", "").strip() + if not tzname or tzname not in pytz.all_timezones_set: + # link will be something like /usr/share/zoneinfo/America/Los_Angeles. + link = os.readlink("/etc/localtime") + tzname = link[link.rfind("zoneinfo/") + 9:] + return pytz.timezone(tzname) + +def get_localzone(): + """Get the computers configured local timezone, if any.""" + global _cache_tz + if _cache_tz is None: + _cache_tz = _get_localzone() + return _cache_tz + +def reload_localzone(): + """Reload the cached localzone. You need to call this if the timezone has changed.""" + global _cache_tz + _cache_tz = _get_localzone() + return _cache_tz + diff --git a/tzlocal/test_data/Harare b/tzlocal/test_data/Harare new file mode 100644 index 0000000000000000000000000000000000000000..258b393637294912a6d6c78973c09424136ed50e GIT binary patch literal 157 zcmWHE%1kq2zyM4@5fBCeMj!^UIhx##rvN#!G9XbI1qK!$-w+08#}E*gA%p~j{sRGC M!?=K^>KbtY08KbtY08KbtY08KbtY08