<commit_before>import itertools
from threading import Lock
from concurrent.futures import Future, CancelledError, TimeoutError

class ClosedError(Exception):
    pass

class _Chain(object):
    """ A linked list of futures.

    Each future yields a result and the next link in the chain
    """
    def __init__(self):
        self._next = Future()

    def push(self, value):
        next_ = _Chain()
        self._next.set_result((value, next_))
        return next_

    def close(self):
        self._next.cancel()

    def wait(self, timeout=None):
        try:
            result = self._next.result(timeout)
        except CancelledError:
            raise ClosedError()
        return result

    def wait_result(self, timeout=None):
        return self.wait(timeout)[0]

    def wait_next(self, timeout=None):
        return self.wait(timeout)[1]

class _SuccessionIterator(object):
    def __init__(self, head, timeout=None):
        self._next = head
        self._timeout = timeout

    def __iter__(self):
        return self

    def __next__(self):
        try:
            result, self._next = self._next.wait(self._timeout)
            return result
        except ClosedError:
            raise StopIteration()

class Succession(object):
    def __init__(self, initial=None, compress=None):
        self._lock = Lock()
        self._compress_function = compress

        self._prelude = []
        self._root = _Chain()
        self._cursor = self._root

    def _head(self):
        """Same as `head` but does not attempt to acquire the succession lock
        """
        try:
            yield from self._iter(timeout=0)
        except TimeoutError:
            raise StopIteration()

    def head(self):
        """Returns a non-blocking iterator over all items that have already
        been added to the succession.
        """
        with self._lock:
            return self._head()

    def _iter(self, timeout=None):
        """Returns an iterator over items in the succession without acquiring
        the succession lock.
        """
        return itertools.chain(
            self._prelude, _SuccessionIterator(self._root, timeout)
        )

    def iter(self, timeout=None):
        """Returns an iterator over items in the succession.  Should be used
        instead of :py:func:`iter` if a timeout is desired.

        :param timeout:
            The time calls to :py:func:`next` should wait for an item before
            raising a :py:exception:`TimeoutError` exception.  If not provided
            the iterator will block indefinitely.
        """
        with self._lock:
            return self._iter(timeout=timeout)

    def __iter__(self):
        return self.iter()

    def push(self, value):
        with self._lock:
            self._cursor = self._cursor.push(value)
            if self._compress_function is not None:
                self._prelude = self._compress_function(self._head())
                self._root = self._cursor

    def close(self):
        """Stop further writes and notify all waiting listeners
        """
        with self._lock:
            self._cursor.close()

    def compress(self, function):
        """Applies function to an iterable of items in the succession and
        replaces the items with the result.

        :param function:
            A function taking an iterable of items and returning an equivalent
            iterable of items to replace them with
        """
        with self._lock:
            self._prelude = list(function(self._head()))
            self._root = self._cursor

    def drop(self):
        with self._lock:
            dropped = self._head()
            self._prelude = []
            self._root = self._cursor
            return dropped

__all__ = ['ClosedError', 'TimeoutError', 'Succession']
<commit_msg>documentation for drop<commit_after>import itertools
from threading import Lock
from concurrent.futures import Future, CancelledError, TimeoutError

class ClosedError(Exception):
    pass

class _Chain(object):
    """ A linked list of futures.

    Each future yields a result and the next link in the chain
    """
    def __init__(self):
        self._next = Future()

    def push(self, value):
        next_ = _Chain()
        self._next.set_result((value, next_))
        return next_

    def close(self):
        self._next.cancel()

    def wait(self, timeout=None):
        try:
            result = self._next.result(timeout)
        except CancelledError:
            raise ClosedError()
        return result

    def wait_result(self, timeout=None):
        return self.wait(timeout)[0]

    def wait_next(self, timeout=None):
        return self.wait(timeout)[1]

class _SuccessionIterator(object):
    def __init__(self, head, timeout=None):
        self._next = head
        self._timeout = timeout

    def __iter__(self):
        return self

    def __next__(self):
        try:
            result, self._next = self._next.wait(self._timeout)
            return result
        except ClosedError:
            raise StopIteration()

class Succession(object):
    def __init__(self, initial=None, compress=None):
        self._lock = Lock()
        self._compress_function = compress

        self._prelude = []
        self._root = _Chain()
        self._cursor = self._root

    def _head(self):
        """Same as `head` but does not attempt to acquire the succession lock
        """
        try:
            yield from self._iter(timeout=0)
        except TimeoutError:
            raise StopIteration()

    def head(self):
        """Returns a non-blocking iterator over all items that have already
        been added to the succession.
        """
        with self._lock:
            return self._head()

    def _iter(self, timeout=None):
        """Returns an iterator over items in the succession without acquiring
        the succession lock.
        """
        return itertools.chain(
            self._prelude, _SuccessionIterator(self._root, timeout)
        )

    def iter(self, timeout=None):
        """Returns an iterator over items in the succession.  Should be used
        instead of :py:func:`iter` if a timeout is desired.

        :param timeout:
            The time calls to :py:func:`next` should wait for an item before
            raising a :py:exception:`TimeoutError` exception.  If not provided
            the iterator will block indefinitely.
        """
        with self._lock:
            return self._iter(timeout=timeout)

    def __iter__(self):
        return self.iter()

    def push(self, value):
        with self._lock:
            self._cursor = self._cursor.push(value)
            if self._compress_function is not None:
                self._prelude = self._compress_function(self._head())
                self._root = self._cursor

    def close(self):
        """Stop further writes and notify all waiting listeners
        """
        with self._lock:
            self._cursor.close()

    def compress(self, function):
        """Applies function to an iterable of items in the succession and
        replaces the items with the result.

        :param function:
            A function taking an iterable of items and returning an equivalent
            iterable of items to replace them with
        """
        with self._lock:
            self._prelude = list(function(self._head()))
            self._root = self._cursor

    def drop(self):
        """Remove all items from the succession

        This does not affect existing iterators over the succession.
        """
        with self._lock:
            dropped = self._head()
            self._prelude = []
            self._root = self._cursor
            return dropped

__all__ = ['ClosedError', 'TimeoutError', 'Succession']
<|endoftext|>


<commit_before># Copyright (c) 2016 Uber Technologies, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from __future__ import absolute_import

import mock
import pytest
from tchannel.event import EventHook
import tornado
import tornado.gen

from tchannel import retry
from tchannel.errors import BusyError
from tchannel.errors import TChannelError
from tchannel.errors import TimeoutError
from tchannel.messages import ErrorCode
from tchannel.tornado import Request
from tchannel.tornado import TChannel
from tchannel.tornado.stream import InMemStream
from six.moves import range

@tornado.gen.coroutine
def handler_error(request, response):
    yield tornado.gen.sleep(0.01)
    yield response.connection.send_error(BusyError("retry", request.id))
    # stop normal response streams
    response.set_exception(TChannelError("stop stream"))

@tornado.gen.coroutine
def handler_success(request, response):
    response.set_body_s(InMemStream("success"))

def server(endpoint):
    tchannel_server = TChannel(name='testserver', hostport='localhost:0')
    tchannel_server.register(endpoint, 'raw', handler_error)
    tchannel_server.listen()
    return tchannel_server

@tornado.gen.coroutine
def chain(number_of_peers, endpoint):
    tchannel = TChannel(name='test')
    for i in range(number_of_peers):
        # Connect to these peers so that they are treated as higher priority
        # than other peers.
        yield tchannel.peers.get(server(endpoint).hostport).connect()
    raise tornado.gen.Return(tchannel)

@pytest.mark.gen_test
def test_retry_timeout():
    endpoint = b'tchannelretrytest'
    tchannel = yield chain(3, endpoint)
    with mock.patch(
        'tchannel.tornado.Request.should_retry_on_error',
        autospec=True,
    ) as mock_should_retry_on_error:
        mock_should_retry_on_error.return_value = True
        with pytest.raises(TimeoutError):
            yield tchannel.request().send(
                endpoint,
                "test",
                "test",
                headers={
                    're': retry.CONNECTION_ERROR_AND_TIMEOUT
                },
                ttl=0.005,
                retry_limit=2,
            )

@pytest.mark.gen_test
def test_retry_on_error_fail():
    endpoint = b'tchannelretrytest'
    tchannel = yield chain(3, endpoint)

    with mock.patch(
        'tchannel.tornado.Request.should_retry_on_error',
        autospec=True,
    ) as mock_should_retry_on_error:
        mock_should_retry_on_error.return_value = True
        with pytest.raises(BusyError) as e:
            yield tchannel.request().send(
                endpoint,
                "test",
                "test",
                headers={
                    're': retry.CONNECTION_ERROR_AND_TIMEOUT
                },
                ttl=1,
                retry_limit=2,
            )

        assert mock_should_retry_on_error.called
        assert mock_should_retry_on_error.call_count == 3
        assert e.value.code == ErrorCode.busy

class MyTestHook(EventHook):
    def __init__(self):
        self.received_response = 0
        self.received_error = 0

    def after_receive_response(self, request, response):
        self.received_response += 1
        assert request.id == response.id

    def after_receive_error(self, request, error):
        self.received_error += 1
        assert request.id == error.id

@pytest.mark.gen_test
@mock.patch('tchannel.tornado.Request.should_retry_on_error')
def test_retry_on_error_success(mock_should_retry_on_error):
    mock_should_retry_on_error.return_value = True

    endpoint = b'tchannelretrytest'
    tchannel = yield chain(2, endpoint)
    hook = MyTestHook()
    tchannel.hooks.register(hook)

    tchannel_success = TChannel(name='test', hostport='localhost:0')
    tchannel_success.register(endpoint, 'raw', handler_success)
    tchannel_success.listen()
    tchannel.peers.get(tchannel_success.hostport)

    response = yield tchannel.request().send(
        endpoint,
        "test",
        "test",
        headers={
            're': retry.CONNECTION_ERROR_AND_TIMEOUT,
        },
        ttl=1,
        retry_limit=2,
    )

    header = yield response.get_header()
    body = yield response.get_body()
    assert body == "success"
    assert header == ""

    assert hook.received_response == 1
    assert hook.received_error == 2

@pytest.mark.gen_test
@pytest.mark.parametrize('retry_flag, error_code, result', [
    (retry.CONNECTION_ERROR, ErrorCode.busy, True),
    (retry.CONNECTION_ERROR, ErrorCode.declined, True),
    (retry.CONNECTION_ERROR, ErrorCode.timeout, False),
    (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.timeout, True),
    (retry.TIMEOUT, ErrorCode.unexpected, False),
    (retry.TIMEOUT, ErrorCode.network_error, False),
    (retry.CONNECTION_ERROR, ErrorCode.network_error, True),
    (retry.NEVER, ErrorCode.network_error, False),
    (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.cancelled, False),
    (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.bad_request, False),
    (retry.CONNECTION_ERROR, ErrorCode.fatal, True),
    (retry.TIMEOUT, ErrorCode.fatal, False),
    (retry.TIMEOUT, ErrorCode.unhealthy, False),
    (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.unhealthy, False),
],
    ids=lambda arg: str(arg)
)
def test_should_retry_on_error(retry_flag, error_code, result):
    request = Request(
        headers={'re': retry_flag},
    )

    error = TChannelError.from_code(error_code, description="retry")
    assert request.should_retry_on_error(error) == result
<commit_msg>Changing endpoints to strings<commit_after># Copyright (c) 2016 Uber Technologies, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

from __future__ import absolute_import

import mock
import pytest
from tchannel.event import EventHook
import tornado
import tornado.gen

from tchannel import retry
from tchannel.errors import BusyError
from tchannel.errors import TChannelError
from tchannel.errors import TimeoutError
from tchannel.messages import ErrorCode
from tchannel.tornado import Request
from tchannel.tornado import TChannel
from tchannel.tornado.stream import InMemStream
from six.moves import range

@tornado.gen.coroutine
def handler_error(request, response):
    yield tornado.gen.sleep(0.01)
    yield response.connection.send_error(BusyError("retry", request.id))
    # stop normal response streams
    response.set_exception(TChannelError("stop stream"))

@tornado.gen.coroutine
def handler_success(request, response):
    response.set_body_s(InMemStream("success"))

def server(endpoint):
    tchannel_server = TChannel(name='testserver', hostport='localhost:0')
    tchannel_server.register(endpoint, 'raw', handler_error)
    tchannel_server.listen()
    return tchannel_server

@tornado.gen.coroutine
def chain(number_of_peers, endpoint):
    tchannel = TChannel(name='test')
    for i in range(number_of_peers):
        # Connect to these peers so that they are treated as higher priority
        # than other peers.
        yield tchannel.peers.get(server(endpoint).hostport).connect()
    raise tornado.gen.Return(tchannel)

@pytest.mark.gen_test
def test_retry_timeout():
    endpoint = 'tchannelretrytest'
    tchannel = yield chain(3, endpoint)
    with mock.patch(
        'tchannel.tornado.Request.should_retry_on_error',
        autospec=True,
    ) as mock_should_retry_on_error:
        mock_should_retry_on_error.return_value = True
        with pytest.raises(TimeoutError):
            yield tchannel.request().send(
                endpoint,
                "test",
                "test",
                headers={
                    're': retry.CONNECTION_ERROR_AND_TIMEOUT
                },
                ttl=0.005,
                retry_limit=2,
            )

@pytest.mark.gen_test
def test_retry_on_error_fail():
    endpoint = 'tchannelretrytest'
    tchannel = yield chain(3, endpoint)

    with mock.patch(
        'tchannel.tornado.Request.should_retry_on_error',
        autospec=True,
    ) as mock_should_retry_on_error:
        mock_should_retry_on_error.return_value = True
        with pytest.raises(BusyError) as e:
            yield tchannel.request().send(
                endpoint,
                "test",
                "test",
                headers={
                    're': retry.CONNECTION_ERROR_AND_TIMEOUT
                },
                ttl=1,
                retry_limit=2,
            )

        assert mock_should_retry_on_error.called
        assert mock_should_retry_on_error.call_count == 3
        assert e.value.code == ErrorCode.busy

class MyTestHook(EventHook):
    def __init__(self):
        self.received_response = 0
        self.received_error = 0

    def after_receive_response(self, request, response):
        self.received_response += 1
        assert request.id == response.id

    def after_receive_error(self, request, error):
        self.received_error += 1
        assert request.id == error.id

@pytest.mark.gen_test
@mock.patch('tchannel.tornado.Request.should_retry_on_error')
def test_retry_on_error_success(mock_should_retry_on_error):
    mock_should_retry_on_error.return_value = True

    endpoint = 'tchannelretrytest'
    tchannel = yield chain(2, endpoint)
    hook = MyTestHook()
    tchannel.hooks.register(hook)

    tchannel_success = TChannel(name='test', hostport='localhost:0')
    tchannel_success.register(endpoint, 'raw', handler_success)
    tchannel_success.listen()
    tchannel.peers.get(tchannel_success.hostport)

    response = yield tchannel.request().send(
        endpoint,
        "test",
        "test",
        headers={
            're': retry.CONNECTION_ERROR_AND_TIMEOUT,
        },
        ttl=1,
        retry_limit=2,
    )

    header = yield response.get_header()
    body = yield response.get_body()
    assert body == "success"
    assert header == ""

    assert hook.received_response == 1
    assert hook.received_error == 2

@pytest.mark.gen_test
@pytest.mark.parametrize('retry_flag, error_code, result', [
    (retry.CONNECTION_ERROR, ErrorCode.busy, True),
    (retry.CONNECTION_ERROR, ErrorCode.declined, True),
    (retry.CONNECTION_ERROR, ErrorCode.timeout, False),
    (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.timeout, True),
    (retry.TIMEOUT, ErrorCode.unexpected, False),
    (retry.TIMEOUT, ErrorCode.network_error, False),
    (retry.CONNECTION_ERROR, ErrorCode.network_error, True),
    (retry.NEVER, ErrorCode.network_error, False),
    (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.cancelled, False),
    (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.bad_request, False),
    (retry.CONNECTION_ERROR, ErrorCode.fatal, True),
    (retry.TIMEOUT, ErrorCode.fatal, False),
    (retry.TIMEOUT, ErrorCode.unhealthy, False),
    (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.unhealthy, False),
],
    ids=lambda arg: str(arg)
)
def test_should_retry_on_error(retry_flag, error_code, result):
    request = Request(
        headers={'re': retry_flag},
    )

    error = TChannelError.from_code(error_code, description="retry")
    assert request.should_retry_on_error(error) == result
<|endoftext|>


<commit_before>import json
import os
from httplib2 import Http
import mimetypes
import hashlib

from apiclient import discovery, errors
from apiclient.http import MediaFileUpload

from oauth2client.client import OAuth2Credentials

class DirectorySync(object):
    def __init__(self, drive, path, folder_id, callback):
        self.drive = drive
        self.fs_path = path
        self.drive_folder_id = folder_id
        self.conflict_callback = callback

    def sync_dir(self, path=None, folder_id=None):
        import pdb; pdb.set_trace()
        if path is None:
            path = self.fs_path
        if folder_id is None:
            folder_id = self.drive_folder_id

        if folder_id is None:
            folder_id = self.drive.create_folder(os.path.basename(path))["id"]

        fs_entries = [os.stat(os.path.join(path, x)) for x in os.listdir(path)]
        fs_entry_map = {x.name: x for x in fs_entries}
        fs_files = [x.name for x in fs_entries if x.is_file()]
        fs_dirs = [x.name for x in fs_entries if x.is_dir()]

        drive_entries = list(self.search(parents=folder_id, trashed=True))
        drive_entry_map = {x["name"]: x for x in drive_entries }
        drive_files = [x["name"] for x in drive_entries if x["mimeType"] != self.folder_mime]
        drive_folders = [x["name"] for x in drive_entries if x["mimeType"] == self.folder_mime]

    def sync_file(self, fs_file, drive_file):
        if os.path.isfile(fs_file.path) and not drive_file["trashed"]: #Both exist
            if md5(fs_path) == drive_file["md5Checksum"]:
                return
            print "Both do not have the same time"

    def fs_get_info(self, path):
        stat = os.stat(path)
        return {
            "name": os.basename(path),
            "md5Checksum": self.md5(path),
            "lastModified": datetime.utcfromtimestamp(stat.st_mtime),
        }

    def md5(self, path):
        hash_md5 = hashlib.md5()
        with open(path, 'rb') as f:
            buf = f.read(65536)
            while len(buf) > 0:
                hash_md5.update(buf)
                buf = f.read(blocksize)
            return hash_md5.digest()

class Drive(object):
    public_folder = "GAPI4TermPublic"
    folder_mime = "application/vnd.google-apps.folder"

    def __init__(self, cred):
        credentials = OAuth2Credentials.from_json(json.dumps(cred))
        http = credentials.authorize(Http())
        self.service = discovery.build("drive", "v3", http=http)

    def share_public_link(self, file_id):
        res = self.share(file_id, role="reader", type="anyone")
        fields = ["webContentLink"]
        file_obj = self.get_file(file_id, fields=fields)
        return file_obj["webContentLink"]

    def public_upload(self, path):
        parent_id = self.get_public_dir()
        return self.create_file(path, parents=parent_id)

    def get_public_dir(self):
        folders = self.search(parents="root", isFolder=True, name=self.public_folder)
        if len(folders):
            return folders[0]["id"]
        else:
            return self.create_public_dir()

    def create_public_dir(self):
        opts = {
            "parents": ["root"],
            "description": "Public folder created by GAPI4Term"
        }
        return self.create_folder(self.public_folder, **opts)["id"]

    def sync_dir(self, folder_id, path, callback):
        ds = DirectorySync(self, path, folder_id, callback)
        ds.sync_dir()

    def iterate_dir(self, dir_id, parents=None):
        if parents is None:
            parents = []
        for entry in self.list_dir(dir_id):
            yield entry, parents
            if entry["mimeType"] == self.folder_mime:
                parents.insert(0, dir_id)
                recur_res = self.iterate_dir(entry["id"], parents=parents)
                for child, parents in recur_res:
                    yield child, parents
                parents.pop(0)

    def list_dir(self, dir_id):
        for obj in self.search(parents=dir_id):
            yield obj

    ###########################
    # CORE FUNCTIONALITY BELOW
    #

    def create_folder(self, name, **kwargs):
        kwargs["mimeType"] = self.folder_mime
        kwargs["name"] = name
        return self.create_file(None, **kwargs)

    def create_file(self, path, **kwargs):
        if "mimeType" not in kwargs and path:
            mime, encoding = mimetypes.guess_type(path)
            if not mime and not encoding:
                mime = "application/octet-stream"
            kwargs["mimeType"] = mime

        if "name" not in kwargs and path:
            kwargs["name"] = os.path.basename(path)

        if "title" not in kwargs and "name" in kwargs:
            kwargs["title"] = kwargs["name"]

        if "parents" in kwargs and isinstance(kwargs["parents"], basestring):
            kwargs["parents"] = [kwargs["parents"]]

        if path:
            media = MediaFileUpload(path, mimetype=kwargs["mimeType"])
            return self.service.files().create(media_body=media, body=kwargs).execute()
        else:
            return self.service.files().create(body=kwargs).execute()

    def get_file(self, file_id, fields=None):
        field_list = ["id", "mimeType", "name", "createdTime", "modifiedTime",
                "version", "md5Checksum"]
        if fields is not None:
            field_list = field_list + fields
        fields_str = ",".join(field_list)
        return self.service.files().get(fileId=file_id,fields=fields_str).execute()

    def search(self, trashed=False, *args, **kwargs):
        query = list(args)
        kwargs["trashed"] = trashed

        if "isFolder" in kwargs:
            if kwargs.pop("isFolder"):
                kwargs["mimeType"] = self.folder_mime

        if "parents" in kwargs:
            if isinstance(kwargs["parents"], basestring):
                kwargs["parents"] = [kwargs["parents"]]
            for parent in kwargs.pop("parents"):
                query.append("'{}' in parents".format(parent))

        for key, value in kwargs.items():
            if isinstance(value, basestring):
                query.append("{} = '{}'".format(key, value))
            elif type(value) == bool:
                query.append("{} = {}".format(key, str(value).lower()))
            elif type(value) == int:
                query.append("{} = {}".format(key, value))
        query_str = " and ".join(query)
        #TODO: Make this into a generator that automatically uses nextPageToken
        return self.service.files().list(q=query_str).execute()["files"]

    def share(self, file_id, **kwargs):
        return self.service.permissions().create(fileId=file_id, body=kwargs).execute()

<commit_msg>[WIP] Temporary commit.<commit_after>import json
import os
from httplib2 import Http
import mimetypes
import hashlib
from datetime import datetime

from apiclient import discovery, errors
from apiclient.http import MediaFileUpload

from oauth2client.client import OAuth2Credentials

class DirectorySync(object):
    def __init__(self, drive, path, folder_id, callback):
        self.drive = drive
        self.fs_path = path
        self.drive_folder_id = folder_id
        self.conflict_callback = callback

    def sync_dir(self, path=None, folder_id=None):
        import pdb; pdb.set_trace()
        if path is None:
            path = self.fs_path
        if folder_id is None:
            folder_id = self.drive_folder_id

        if folder_id is None:
            folder_id = self.drive.create_folder(os.path.basename(path))["id"]

        fs_entries = [self.fs_get_info(os.path.join(path, x)) for x in os.listdir(path)]
        fs_entry_map = {x["name"]: x for x in fs_entries}
        fs_files = {x["name"] for x in fs_entries if x["isFile"]}
        fs_dirs = {x["name"] for x in fs_entries if x["isDir"]}

        drive_entries = list(self.drive.search(parents=folder_id, trashed=True))
        drive_entry_map = {x["name"]: x for x in drive_entries }
        drive_files = {x["name"] for x in drive_entries if x["mimeType"] != self.folder_mime}
        drive_dirs = {x["name"] for x in drive_entries if x["mimeType"] == self.folder_mime}

        for name in drive_files | fs_files:
            self.sync_file(name, fs_entry_map.get(name), drive_entry_map.get(name))
        for file_name in set(fs_files) - set(drive_files): #File in FS but not in Drive: Upload
            self.drive.create_file(name=name, parents=folder_id)

        for file_name in set(drive_files) - set(fs_files): #File in Drive but not in FS: Conflict
            if drive_entries[file_name]['trashed']:
                os.remove(drive_entries[file_name]["path"])

        for dir_name in set(fs_dirs) - set(drive_dirs): #Exists in FS, not in drive: Create
            self.drive.create_folder(dir_name, parents=folder_id)

    def sync_file(self, name, fs_file, drive_file):
        if fs_file is None and drive_file:
            if drive_file["trashed"]:
                return

        if os.path.isfile(fs_file.path) and not drive_file["trashed"]: #Both exist
            if md5(fs_path) == drive_file["md5Checksum"]:
                return
            print "Both do not have the same time"

    def fs_get_info(self, path):
        stat = os.stat(path)
        return {
            "isFile": os.path.isfile(path),
            "isDir": os.path.isdir(path),
            "name": os.path.basename(path),
            "path": path,
            "md5Checksum": self.md5(path),
            "lastModified": datetime.utcfromtimestamp(stat.st_mtime),
        }

    def md5(self, path, blocksize=65536):
        hash_md5 = hashlib.md5()
        with open(path, 'rb') as f:
            buf = f.read(blocksize)
            while len(buf) > 0:
                hash_md5.update(buf)
                buf = f.read(blocksize)
            return hash_md5.digest()

class Drive(object):
    public_folder = "GAPI4TermPublic"
    folder_mime = "application/vnd.google-apps.folder"

    def __init__(self, cred):
        credentials = OAuth2Credentials.from_json(json.dumps(cred))
        http = credentials.authorize(Http())
        self.service = discovery.build("drive", "v3", http=http)

    def share_public_link(self, file_id):
        res = self.share(file_id, role="reader", type="anyone")
        fields = ["webContentLink"]
        file_obj = self.get_file(file_id, fields=fields)
        return file_obj["webContentLink"]

    def public_upload(self, path):
        parent_id = self.get_public_dir()
        return self.create_file(path, parents=parent_id)

    def get_public_dir(self):
        folders = self.search(parents="root", isFolder=True, name=self.public_folder)
        if len(folders):
            return folders[0]["id"]
        else:
            return self.create_public_dir()

    def create_public_dir(self):
        opts = {
            "parents": ["root"],
            "description": "Public folder created by GAPI4Term"
        }
        return self.create_folder(self.public_folder, **opts)["id"]

    def sync_dir(self, folder_id, path, callback):
        ds = DirectorySync(self, path, folder_id, callback)
        ds.sync_dir()

    def iterate_dir(self, dir_id, parents=None):
        if parents is None:
            parents = []
        for entry in self.list_dir(dir_id):
            yield entry, parents
            if entry["mimeType"] == self.folder_mime:
                parents.insert(0, dir_id)
                recur_res = self.iterate_dir(entry["id"], parents=parents)
                for child, parents in recur_res:
                    yield child, parents
                parents.pop(0)

    def list_dir(self, dir_id):
        for obj in self.search(parents=dir_id):
            yield obj

    ###########################
    # CORE FUNCTIONALITY BELOW
    #

    def create_folder(self, name, **kwargs):
        kwargs["mimeType"] = self.folder_mime
        kwargs["name"] = name
        return self.create_file(None, **kwargs)

    def create_file(self, path, **kwargs):
        if "mimeType" not in kwargs and path:
            mime, encoding = mimetypes.guess_type(path)
            if not mime and not encoding:
                mime = "application/octet-stream"
            kwargs["mimeType"] = mime

        if "name" not in kwargs and path:
            kwargs["name"] = os.path.basename(path)

        if "title" not in kwargs and "name" in kwargs:
            kwargs["title"] = kwargs["name"]

        if "parents" in kwargs and isinstance(kwargs["parents"], basestring):
            kwargs["parents"] = [kwargs["parents"]]

        if path:
            media = MediaFileUpload(path, mimetype=kwargs["mimeType"])
            return self.service.files().create(media_body=media, body=kwargs).execute()
        else:
            return self.service.files().create(body=kwargs).execute()

    def get_file(self, file_id, fields=None):
        field_list = ["id", "mimeType", "name", "createdTime", "modifiedTime",
                "version", "md5Checksum"]
        if fields is not None:
            field_list = field_list + fields
        fields_str = ",".join(field_list)
        return self.service.files().get(fileId=file_id,fields=fields_str).execute()

    def search(self, trashed=False, *args, **kwargs):
        query = list(args)
        kwargs["trashed"] = trashed

        if "isFolder" in kwargs:
            if kwargs.pop("isFolder"):
                kwargs["mimeType"] = self.folder_mime

        if "parents" in kwargs:
            if isinstance(kwargs["parents"], basestring):
                kwargs["parents"] = [kwargs["parents"]]
            for parent in kwargs.pop("parents"):
                query.append("'{}' in parents".format(parent))

        for key, value in kwargs.items():
            if isinstance(value, basestring):
                query.append("{} = '{}'".format(key, value))
            elif type(value) == bool:
                query.append("{} = {}".format(key, str(value).lower()))
            elif type(value) == int:
                query.append("{} = {}".format(key, value))
        query_str = " and ".join(query)
        #TODO: Make this into a generator that automatically uses nextPageToken
        return self.service.files().list(q=query_str).execute()["files"]

    def share(self, file_id, **kwargs):
        return self.service.permissions().create(fileId=file_id, body=kwargs).execute()

<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
"""
Created on Mon Nov 18 13:36:39 2013

input_freqs.py

@author: Christian Münker
Created on 23.1.2015
"""
from __future__ import print_function, division, unicode_literals
import sys, os
from PyQt4 import QtGui

# add main directory from one level above if this file is run as __main__
# for test purposes
if __name__ == "__main__":
    __cwd__ = os.path.dirname(os.path.abspath(__file__))
    sys.path.append(__cwd__ + '/..')

class InputFreqSpecs(QtGui.QWidget):
    """
    Build and update widget for entering the frequency
    specifications like F_sb, F_pb etc.
    """

    def __init__(self, specs, DEBUG = True):

        """
        Initialize
        specs: A dictionary containing all the specs
        labels: Names of the frequency spec labels
        """
        super(InputFreqSpecs, self).__init__()
        self.DEBUG = DEBUG
        self.specs = specs  # dictionary containing _all_ specifications of the
                            # currently selected filter
        self.qlabels = []    # list with references to QLabel widgets
        self.qlineedit = [] # list with references to QLineEdit widgets

        self.initUI()

    def initUI(self):
        self.WVLayout = QtGui.QVBoxLayout() # Widget vertical layout

        title = "Frequency Specifications"

        units = ['Normalized to f_S', 'Normalized to f_S/2',
                       'Hz', 'kHz', 'MHz', 'GHz']

        self.idxOld = -1 # index of comboUnits before last change

        bfont = QtGui.QFont()
        bfont.setBold(True)
#            bfont.setWeight(75)
        self.qtitle = QtGui.QLabel(self) # field for widget title
        self.qtitle.setText(str(title))
        self.qtitle.setFont(bfont)
        self.qtitle.setWordWrap(True)
        self.WVLayout.addWidget(self.qtitle)

        self.labelUnits=QtGui.QLabel(self)
        self.labelUnits.setText("Units")

        self.f_S = self.specs["f_S"]
        self.editF_S = QtGui.QLineEdit()
        self.editF_S.setText(str(self.f_S))
        self.editF_S.setObjectName("f_S")

        self.labelF_S = QtGui.QLabel(self)
        self.labelF_S.setText(self.rtLabel("f_S"))

        self.comboUnits = QtGui.QComboBox(self)
        self.comboUnits.setObjectName("comboUnits")
        self.comboUnits.addItems(units)
        self.comboUnits.setCurrentIndex(0)

        self.butSort = QtGui.QPushButton(self)
        self.butSort.setText("Sort")
        self.butSort.setToolTip("Sort frequencies in ascending order.")
        self.hbox = QtGui.QHBoxLayout()
        self.hbox.addWidget(self.comboUnits)
        self.hbox.addWidget(self.butSort)

        # Create a gridLayout consisting of QLabel and QLineEdit fields
        # for setting f_S, the units and the actual frequency specs:
        self.layout = QtGui.QGridLayout() # sublayout for spec fields
        # addWidget(widget,row,col,rowSpan=1, colSpan=1, QtCore.Qt.Alignxxx)
        self.layout.addWidget(self.labelUnits,0,0)
        self.layout.addLayout(self.hbox,0,1)
        self.layout.addWidget(self.labelF_S,1,0)
        self.layout.addWidget(self.editF_S,1,1)

        # - Build a list from all entries in the specs dictionary starting
        #   with "F" (= frequency specifications of the current filter)
        # - Pass the list to setEntries which recreates the widget
        newLabels = [l for l in self.specs if l[0] == 'F']
        self.setEntries(newLabels = newLabels)

        sfFrame = QtGui.QFrame()
        sfFrame.setFrameStyle(QtGui.QFrame.StyledPanel|QtGui.QFrame.Sunken)
        sfFrame.setLayout(self.layout)

        self.WVLayout.addWidget(sfFrame)
        self.setLayout(self.WVLayout)

#        self.WVLayout.addLayout(self.layout) # no frame
#        mainLayout = QtGui.QHBoxLayout()
#        mainLayout.addWidget(sfFrame)
#        self.setLayout(mainLayout)

        # SIGNALS & SLOTS
        # Every time a field is edited, call self.freqUnits - the signal is
        #   constructed in _addEntry
        self.comboUnits.currentIndexChanged.connect(self.freqUnits)
        self.editF_S.editingFinished.connect(self.freqUnits)
        self.butSort.clicked.connect(self._sortEntries)

        self.freqUnits()

    def mousePressEvent(self, event):
        """
        Do something every time a mouse event happens inside this widget - but
        only if the event isn't swallowed by a child widget!!
        """
        print ("InputFreqs Mouse Press")
        super(InputFreqs, self).mousePressEvent(event)

#-------------------------------------------------------------
    def freqUnits(self):
        """
        Transform the frequency spec input fields according to the Units
        setting. Spec entries are always stored normalized w.r.t. f_S in the
        dictionary; when f_S or the unit are changed, only the displayed values
        of the frequency entries are updated, not the dictionary!

        freqUnits is called during init and every time a widget sends a signal.
        """
        idx = self.comboUnits.currentIndex()  # read index of units combobox
        self.f_S = float(self.editF_S.text()) # read sampling frequency

        if self.sender(): # origin of signal that triggered the slot
            senderName = self.sender().objectName()
            print(senderName + ' was triggered\n================')
        else: # no sender, freqUnits has been called from initUI
            senderName = "comboUnits"

        if senderName == "f_S" and self.f_S != 0:
            # f_S has been edited -> change display of frequency entries and
            # dictionary entry for f_S
            self.specs['f_S'] = self.f_S
            for i in range(len(self.qlineedit)):
                f = self.specs[self.qlineedit[i].objectName()]
                self.qlineedit[i].setText(str(f * self.f_S))

        elif senderName == "comboUnits" and idx != self.idxOld:
            # combo unit has changed -> change display of frequency entries
            self.editF_S.setVisible(idx > 1)  # only visible when
            self.labelF_S.setVisible(idx > 1) # not normalized

            if self.idxOld == 1: # was: normalized to f_S/2,
                # remove scaling factor 2 from spec entries
                for i in range(len(self.qlineedit)):
                    f = self.specs[self.qlineedit[i].objectName()]
                    self.qlineedit[i].setText(str(f))
                self.f_S = 1.

            if idx < 2: # normalized frequency
                if idx == 0: # normalized to f_S
                    self.f_S = 1.
                    fLabel = r"$F = f/f_S \; \rightarrow$"
                else:   # idx == 1: normalized to f_S / 2
                    self.f_S = 2.
                    fLabel = r"$F = 2f/f_S \; \rightarrow$"

                self.editF_S.setText(str(self.f_S)) # update textedit

                # recalculate displayed freq spec values but do not store them:
                for i in range(len(self.qlineedit)):
                    f = self.specs[self.qlineedit[i].objectName()]
                    self.qlineedit[i].setText(str(f * self.f_S))
            else: # Hz, kHz, ...
                unit = str(self.comboUnits.itemText(idx))
                fLabel = r"$f$ in " + unit + r"$\; \rightarrow$"

            self.specs.update({"plt_fLabel":fLabel}) # label for freq. axis
            self.specs['f_S'] = self.f_S # store f_S in dictionary
            self.editF_S.setText(str(self.f_S))

        else: # freq. spec textfield has been changed
            self.storeEntries()

#        print(idx, self.f_S, self.qlineedit[0].text(), self.specs["F_pb"])
        self.idxOld = idx # remember setting of comboBox
        self.f_S_old = self.f_S # and f_S (not used yet)

    def rtLabel(self, label):
        """
        Rich text labels: Format labels with HTML tags, replacing '_' by
        HTML subscript tags
        """
        #"{0}".format(newLabels[i])) # update label
        if "_" in label:
            label = label.replace('_', '')
            label += ""
        htmlLabel = ""+label+""
        return htmlLabel

    def setEntries(self, newLabels = []):
        """
        Set labels and get corresponding values from filter dictionary.
        When number of elements changes, the layout of subwidget is rebuilt.
        """
        # Check whether the number of entries has changed
        for i in range(max(len(self.qlabels), len(newLabels))):
             # newLabels is shorter than qlabels -> delete the difference
            if (i > (len(newLabels)-1)):
                self._delEntry(len(newLabels))

            # newLabels is longer than existing qlabels -> create new ones!
            elif (i > (len(self.qlabels)-1)):
             self._addEntry(i,newLabels[i])

            else:
                # when entry has changed, update label and corresponding value
                if self.qlineedit[i].objectName() != newLabels[i]:
                    self.qlabels[i].setText(self.rtLabel(newLabels[i]))
                    self.qlineedit[i].setText(str(self.specs[newLabels[i]]*self.f_S))
                    self.qlineedit[i].setObjectName(newLabels[i])  # update ID

    def _delEntry(self,i):
        """
        Delete entry number i from subwidget (QLabel and QLineEdit)
        """
        self.layout.removeWidget(self.qlabels[i])
        self.layout.removeWidget(self.qlineedit[i])

        self.qlabels[i].deleteLater()
        del self.qlabels[i]
        self.qlineedit[i].deleteLater()
        del self.qlineedit[i]

    def _addEntry(self, i, newLabel):
        """
        Append entry number i to subwidget (QLabel und QLineEdit)
        """
        self.qlabels.append(QtGui.QLabel(self))
        self.qlabels[i].setText(self.rtLabel(newLabel))

        self.qlineedit.append(QtGui.QLineEdit(str(self.specs[newLabel]*self.f_S)))
        self.qlineedit[i].editingFinished.connect(self.freqUnits)
        self.qlineedit[i].setObjectName(newLabel) # update ID

        self.layout.addWidget(self.qlabels[i],(i+2),0)
        self.layout.addWidget(self.qlineedit[i],(i+2),1)

    def _sortEntries(self):
        """
        Sort spec entries with ascending frequency and store in filter dict.
        """
        fSpecs = [self.qlineedit[i].text() for i in range(len(self.qlineedit))]

        fSpecs.sort()

        for i in range(len(self.qlineedit)):
            self.qlineedit[i].setText(fSpecs[i])

        self.storeEntries()

    def loadEntries(self):
        """
        Reload textfields from filter dictionary to update changed settings
        """
        for i in range(len(self.qlineedit)):
            self.qlineedit[i].setText(
                str(self.specs[self.qlineedit[i].objectName()] * self.f_S))

    def storeEntries(self):
        """
        Store specification entries in filter dictionary
        Entries are normalized with sampling frequency self.f_S !
        The scale factor (khz, ...) is contained neither in f_S nor the specs
        hence, it cancels out.
        """
        for i in range(len(self.qlineedit)):
            self.specs.update(
                {self.qlineedit[i].objectName():
                    float(self.qlineedit[i].text())/self.f_S})

#------------------------------------------------------------------------------

if __name__ == '__main__':
    import filterbroker as fb
    app = QtGui.QApplication(sys.argv)
    form = InputFreqSpecs(specs = fb.gD["selFilter"])

    form.setEntries(newLabels = ['F_SB','F_SB2','F_PB','F_PB2'])
    form.setEntries(newLabels = ['F_PB','F_PB2'])

    form.show()

    app.exec_()
<commit_msg>bugfix, class was referenced with wrong name Signed-off-by: chipmuenk <mail@chipmuenk.de><commit_after># -*- coding: utf-8 -*-
"""
Created on Mon Nov 18 13:36:39 2013

input_freqs.py

@author: Christian Münker
Created on 23.1.2015
"""
from __future__ import print_function, division, unicode_literals
import sys, os
from PyQt4 import QtGui

# add main directory from one level above if this file is run as __main__
# for test purposes
if __name__ == "__main__":
    __cwd__ = os.path.dirname(os.path.abspath(__file__))
    sys.path.append(__cwd__ + '/..')

class InputFreqSpecs(QtGui.QWidget):
    """
    Build and update widget for entering the frequency
    specifications like F_sb, F_pb etc.
    """

    def __init__(self, specs, DEBUG = True):

        """
        Initialize
        specs: A dictionary containing all the specs
        labels: Names of the frequency spec labels
        """
        super(InputFreqSpecs, self).__init__()
        self.DEBUG = DEBUG
        self.specs = specs  # dictionary containing _all_ specifications of the
                            # currently selected filter
        self.qlabels = []    # list with references to QLabel widgets
        self.qlineedit = [] # list with references to QLineEdit widgets

        self.initUI()

    def initUI(self):
        self.WVLayout = QtGui.QVBoxLayout() # Widget vertical layout

        title = "Frequency Specifications"

        units = ['Normalized to f_S', 'Normalized to f_S/2',
                       'Hz', 'kHz', 'MHz', 'GHz']

        self.idxOld = -1 # index of comboUnits before last change

        bfont = QtGui.QFont()
        bfont.setBold(True)
#            bfont.setWeight(75)
        self.qtitle = QtGui.QLabel(self) # field for widget title
        self.qtitle.setText(str(title))
        self.qtitle.setFont(bfont)
        self.qtitle.setWordWrap(True)
        self.WVLayout.addWidget(self.qtitle)

        self.labelUnits=QtGui.QLabel(self)
        self.labelUnits.setText("Units")

        self.f_S = self.specs["f_S"]
        self.editF_S = QtGui.QLineEdit()
        self.editF_S.setText(str(self.f_S))
        self.editF_S.setObjectName("f_S")

        self.labelF_S = QtGui.QLabel(self)
        self.labelF_S.setText(self.rtLabel("f_S"))

        self.comboUnits = QtGui.QComboBox(self)
        self.comboUnits.setObjectName("comboUnits")
        self.comboUnits.addItems(units)
        self.comboUnits.setCurrentIndex(0)

        self.butSort = QtGui.QPushButton(self)
        self.butSort.setText("Sort")
        self.butSort.setToolTip("Sort frequencies in ascending order.")

        self.hbox = QtGui.QHBoxLayout()
        self.hbox.addWidget(self.comboUnits)
        self.hbox.addWidget(self.butSort)

        # Create a gridLayout consisting of QLabel and QLineEdit fields
        # for setting f_S, the units and the actual frequency specs:
        self.layout = QtGui.QGridLayout() # sublayout for spec fields
        # addWidget(widget,row,col,rowSpan=1, colSpan=1, QtCore.Qt.Alignxxx)
        self.layout.addWidget(self.labelUnits,0,0)
        self.layout.addLayout(self.hbox,0,1)
        self.layout.addWidget(self.labelF_S,1,0)
        self.layout.addWidget(self.editF_S,1,1)

        # - Build a list from all entries in the specs dictionary starting
        #   with "F" (= frequency specifications of the current filter)
        # - Pass the list to setEntries which recreates the widget
        newLabels = [l for l in self.specs if l[0] == 'F']
        self.setEntries(newLabels = newLabels)

        sfFrame = QtGui.QFrame()
        sfFrame.setFrameStyle(QtGui.QFrame.StyledPanel|QtGui.QFrame.Sunken)
        sfFrame.setLayout(self.layout)

        self.WVLayout.addWidget(sfFrame)
        self.setLayout(self.WVLayout)

#        self.WVLayout.addLayout(self.layout) # no frame
#        mainLayout = QtGui.QHBoxLayout()
#        mainLayout.addWidget(sfFrame)
#        self.setLayout(mainLayout)

        # SIGNALS & SLOTS
        # Every time a field is edited, call self.freqUnits - the signal is
        #   constructed in _addEntry
        self.comboUnits.currentIndexChanged.connect(self.freqUnits)
        self.editF_S.editingFinished.connect(self.freqUnits)
        self.butSort.clicked.connect(self._sortEntries)

        self.freqUnits()

    def mousePressEvent(self, event):
        """
        Do something every time a mouse event happens inside this widget - but
        only if the event isn't swallowed by a child widget!!
        """
        print ("InputFreqs Mouse Press")
        super(InputFreqSpecs, self).mousePressEvent(event)

#-------------------------------------------------------------
    def freqUnits(self):
        """
        Transform the frequency spec input fields according to the Units
        setting. Spec entries are always stored normalized w.r.t. f_S in the
        dictionary; when f_S or the unit are changed, only the displayed values
        of the frequency entries are updated, not the dictionary!

        freqUnits is called during init and every time a widget sends a signal.
        """
        idx = self.comboUnits.currentIndex()  # read index of units combobox
        self.f_S = float(self.editF_S.text()) # read sampling frequency

        if self.sender(): # origin of signal that triggered the slot
            senderName = self.sender().objectName()
            print(senderName + ' was triggered\n================')
        else: # no sender, freqUnits has been called from initUI
            senderName = "comboUnits"

        if senderName == "f_S" and self.f_S != 0:
            # f_S has been edited -> change display of frequency entries and
            # dictionary entry for f_S
            self.specs['f_S'] = self.f_S
            for i in range(len(self.qlineedit)):
                f = self.specs[self.qlineedit[i].objectName()]
                self.qlineedit[i].setText(str(f * self.f_S))

        elif senderName == "comboUnits" and idx != self.idxOld:
            # combo unit has changed -> change display of frequency entries
            self.editF_S.setVisible(idx > 1)  # only visible when
            self.labelF_S.setVisible(idx > 1) # not normalized

            if self.idxOld == 1: # was: normalized to f_S/2,
                # remove scaling factor 2 from spec entries
                for i in range(len(self.qlineedit)):
                    f = self.specs[self.qlineedit[i].objectName()]
                    self.qlineedit[i].setText(str(f))
                self.f_S = 1.

            if idx < 2: # normalized frequency
                if idx == 0: # normalized to f_S
                    self.f_S = 1.
                    fLabel = r"$F = f/f_S \; \rightarrow$"
                else:   # idx == 1: normalized to f_S / 2
                    self.f_S = 2.
                    fLabel = r"$F = 2f/f_S \; \rightarrow$"

                self.editF_S.setText(str(self.f_S)) # update textedit

                # recalculate displayed freq spec values but do not store them:
                for i in range(len(self.qlineedit)):
                    f = self.specs[self.qlineedit[i].objectName()]
                    self.qlineedit[i].setText(str(f * self.f_S))
            else: # Hz, kHz, ...
                unit = str(self.comboUnits.itemText(idx))
                fLabel = r"$f$ in " + unit + r"$\; \rightarrow$"

            self.specs.update({"plt_fLabel":fLabel}) # label for freq. axis
            self.specs['f_S'] = self.f_S # store f_S in dictionary
            self.editF_S.setText(str(self.f_S))

        else: # freq. spec textfield has been changed
            self.storeEntries()

#        print(idx, self.f_S, self.qlineedit[0].text(), self.specs["F_pb"])
        self.idxOld = idx # remember setting of comboBox
        self.f_S_old = self.f_S # and f_S (not used yet)

    def rtLabel(self, label):
        """
        Rich text labels: Format labels with HTML tags, replacing '_' by
        HTML subscript tags
        """
        #"{0}".format(newLabels[i])) # update label
        if "_" in label:
            label = label.replace('_', '')
            label += ""
        htmlLabel = ""+label+""
        return htmlLabel

    def setEntries(self, newLabels = []):
        """
        Set labels and get corresponding values from filter dictionary.
        When number of elements changes, the layout of subwidget is rebuilt.
        """
        # Check whether the number of entries has changed
        for i in range(max(len(self.qlabels), len(newLabels))):
             # newLabels is shorter than qlabels -> delete the difference
            if (i > (len(newLabels)-1)):
                self._delEntry(len(newLabels))

            # newLabels is longer than existing qlabels -> create new ones!
            elif (i > (len(self.qlabels)-1)):
             self._addEntry(i,newLabels[i])

            else:
                # when entry has changed, update label and corresponding value
                if self.qlineedit[i].objectName() != newLabels[i]:
                    self.qlabels[i].setText(self.rtLabel(newLabels[i]))
                    self.qlineedit[i].setText(str(self.specs[newLabels[i]]*self.f_S))
                    self.qlineedit[i].setObjectName(newLabels[i])  # update ID

    def _delEntry(self,i):
        """
        Delete entry number i from subwidget (QLabel and QLineEdit)
        """
        self.layout.removeWidget(self.qlabels[i])
        self.layout.removeWidget(self.qlineedit[i])

        self.qlabels[i].deleteLater()
        del self.qlabels[i]
        self.qlineedit[i].deleteLater()
        del self.qlineedit[i]

    def _addEntry(self, i, newLabel):
        """
        Append entry number i to subwidget (QLabel und QLineEdit)
        """
        self.qlabels.append(QtGui.QLabel(self))
        self.qlabels[i].setText(self.rtLabel(newLabel))

        self.qlineedit.append(QtGui.QLineEdit(str(self.specs[newLabel]*self.f_S)))
        self.qlineedit[i].editingFinished.connect(self.freqUnits)
        self.qlineedit[i].setObjectName(newLabel) # update ID

        self.layout.addWidget(self.qlabels[i],(i+2),0)
        self.layout.addWidget(self.qlineedit[i],(i+2),1)

    def _sortEntries(self):
        """
        Sort spec entries with ascending frequency and store in filter dict.
        """
        fSpecs = [self.qlineedit[i].text() for i in range(len(self.qlineedit))]

        fSpecs.sort()

        for i in range(len(self.qlineedit)):
            self.qlineedit[i].setText(fSpecs[i])

        self.storeEntries()

    def loadEntries(self):
        """
        Reload textfields from filter dictionary to update changed settings
        """
        for i in range(len(self.qlineedit)):
            self.qlineedit[i].setText(
                str(self.specs[self.qlineedit[i].objectName()] * self.f_S))

    def storeEntries(self):
        """
        Store specification entries in filter dictionary
        Entries are normalized with sampling frequency self.f_S !
        The scale factor (khz, ...) is contained neither in f_S nor the specs
        hence, it cancels out.
        """
        for i in range(len(self.qlineedit)):
            self.specs.update(
                {self.qlineedit[i].objectName():
                    float(self.qlineedit[i].text())/self.f_S})

#------------------------------------------------------------------------------

if __name__ == '__main__':
    import filterbroker as fb
    app = QtGui.QApplication(sys.argv)
    form = InputFreqSpecs(specs = fb.gD["selFilter"])

    form.setEntries(newLabels = ['F_SB','F_SB2','F_PB','F_PB2'])
    form.setEntries(newLabels = ['F_PB','F_PB2'])

    form.show()

    app.exec_()
<|endoftext|>


<commit_before>#!/usr/bin/env python

import logging
from os import mkdir
from os.path import join as path_join
from urllib.parse import quote_plus, urljoin
from datetime import datetime

import requests
from bs4 import BeautifulSoup

logging.basicConfig(
    format=(
        '%(asctime)s\t%(levelname)s\t'
        #'%(processName)s\t%(threadName)s\t'
        '%(name)s\t%(funcName)s:%(lineno)d\t'
        '%(message)s'
    ),
    level=logging.DEBUG
)

def _make_fake_browser():

    fake_browser = requests.Session()
    fake_browser.headers = {
        'user-agent': (
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) '
            'AppleWebKit/537.36 (KHTML, like Gecko) '
            'Chrome/54.0.2840.98 Safari/537.36'
        ),
        'accept': (
            'text/html,application/xhtml+xml,application/xml;q=0.9,'
            'image/webp,*/*;q=0.8'
        ),
        'accept-encoding': 'gzip, deflate, sdch, br',
        'accept-language': 'en-US,en;q=0.8,zh-TW;q=0.6,zh;q=0.4',
        'cookie': 'over18=1',
    }

    return fake_browser

_SHARED_FAKE_BROWSER = _make_fake_browser()
_CACHE_DIR_PATH = 'cache/'
_URL_SET_SKIPPING_CACHE = {'https://www.ptt.cc/bbs/Gossiping/index.html'}

def read_or_request(url):

    # should generate valid fname for most of the systems
    fname = quote_plus(url)
    path = path_join(_CACHE_DIR_PATH, fname)

    # try cache

    if url in _URL_SET_SKIPPING_CACHE:

        logging.info('Skip cache for {}'.format(url))

    else:

        try:
            with open(path) as f:
                logging.info('Hit {}'.format(url))
                return f.read()
        except OSError:
            logging.info('Missed {}'.format(url))

    # request

    resp = _SHARED_FAKE_BROWSER.get(url)
    text = resp.text

    while 1:

        try:
            with open(path, 'w') as f:
                f.write(text)
        except FileNotFoundError:
            mkdir(_CACHE_DIR_PATH)
            continue
        else:
            logging.info('Wrote {}'.format(url))

        # only loop once normally
        break

    return text

_ROOT = 'https://www.ptt.cc'

def parse_index_page(text):

    soup = BeautifulSoup(text, 'html.parser')

    # paging

    paging_a_tags = soup.select('.action-bar .btn-group-paging a')

    prev_a_tag = paging_a_tags[1]
    next_a_tag = paging_a_tags[2]

    if '上頁' not in prev_a_tag.string:
        raise RuntimeError('the prev button looks not right')
    if '下頁' not in next_a_tag.string:
        raise RuntimeError('the next button looks not right')

    prev_url = urljoin(_ROOT, prev_a_tag.get('href', ''))
    next_url = urljoin(_ROOT, next_a_tag.get('href', ''))

    # entries

    entry_ds = []
    ent_tags = soup.select('.r-list-container .r-ent')
    for ent_tag in ent_tags:

        # push_count

        # cases
        #
        # 1.
12

        # 2.

        # 3.
X1

        #
        push_count = -65535
        span_tag = ent_tag.find(class_='nrec').find('span')

        # if case 2
        if not span_tag:

            push_count = 0

        else:

            span_text = span_tag.string

            while 1:

                # if case 1
                try:
                    push_count = int(span_text)
                except ValueError:
                    pass
                else:
                    break

                # if case 3
                try:
                    push_count = int(span_text[1:])
                except ValueError:
                    pass
                else:
                    push_count = -(push_count+10)
                    break

                # loop once normally
                break

        # title & article_url

        # cases
        #
        # 1.

        #
        #            Re: [問卦] 有沒有高雄其實比台北進步的八卦??
        #
        #

        # 2.
 (本文已被刪除) [gundam0613]

        #

        title_a_tag = ent_tag.find(class_='title').a
        title = title_a_tag.string

        title_href = title_a_tag.get('href', '')
        if title_href:
            article_url = path_join(_ROOT, title_href)
        else:
            article_url = ''

        # others

        ent_meta_tag = ent_tag.find(class_='meta')

        # mmdd won't contain year!
        raw_mmdd = ent_meta_tag.find(class_='date').string
        author_id = ent_meta_tag.find(class_='author').string

        # append

        entry_ds.append({
            'push_count' : push_count,
            'title'      : title,
            'article_url': article_url,
            'raw_mmdd'   : raw_mmdd,
            'author_id'  : author_id
        })

    return {
        'prev_url': prev_url,
        'next_url': next_url,
        'entry_ds': entry_ds
    }

_EXPECTED_ARTICLE_TAG_KEYS = ('作者', '看板', '標題', '時間')
_PUSH_TAG_STRIPPED_TEXT_SCORE_MAP = {
    '噓': -1,
    '→': 0,
    '推': 1
}

def parse_article_page(text):

    soup = BeautifulSoup(text, 'html.parser')
    main_content_tag = soup.find(id='main-content')

    # meta
    #
    #

    #     作者
    #     a77774444 (我愛ˋ台灣)
    #

    #
    for tag, key in zip(
        main_content_tag.select('.article-meta-tag'),
        _EXPECTED_ARTICLE_TAG_KEYS
    ):
        if tag.string != key:
            raise RuntimeError('the article tags may be changed')

    author_line, board_name, title, timestamp_text = (
        tag.string
        for tag in main_content_tag.select('.article-meta-value')
    )

    author_id, _, author_line_tail = author_line.partition(' (')
    author_nick = author_line_tail[:-1]

    # Tue Nov 29 05:05:03 2016
    # Fri Jul  1 21:12:25 2005
    created_dt = datetime.strptime(timestamp_text, '%a %b %d %H:%M:%S %Y')

    # body

    recording = False
    body_lines = []
    for i, line in enumerate(main_content_tag.stripped_strings):

        if i == 6:
            if line != '時間':
                raise RuntimeError('the article structure may be changed')
        elif i == 8:
            recording = True
        elif line.startswith('※ 發信站: 批踢踢實業坊'):
            recording = False
            break

        if recording:
            body_lines.append(line)

    else:

        raise RuntimeError('the ending text of article may be change/')

    # ... 以後台北人都要去高雄當台勞了。\n\n--'
    body = '\n'.join(body_lines).rstrip('\n-')

    # pushes

    push_ds = [
        {
            'score': _PUSH_TAG_STRIPPED_TEXT_SCORE_MAP.get(
                push_tag.find(class_='push-tag').string.strip(), -255
            ),
            'id': push_tag.find(class_='push-userid').string,
            'text': push_tag.find(class_='push-content').string,
            # won't contain year!
            'raw_mmddhhmm': push_tag.find(class_='push-ipdatetime').string.strip()
        }
        for push_tag in main_content_tag.select('.push')
    ]

    return {
        'board_name' : board_name,
        'author_id'  : author_id,
        'author_nick': author_nick,
        'title'      : title,
        'created_dt' : created_dt,
        'body'       : body,
        'push_ds'    : push_ds
    }

if __name__ == '__main__':

    from pprint import pprint

    # test the index page
    pprint(parse_index_page(read_or_request(
        'https://www.ptt.cc/bbs/Gossiping/index20177.html'
    )))

    # test the article page #1
    pprint(parse_article_page(read_or_request(
        'https://www.ptt.cc/bbs/Gossiping/M.1480367106.A.A55.html'
    )))

    # test the article page #2
    pprint(parse_article_page(read_or_request(
        'https://www.ptt.cc/bbs/Gossiping/M.1480380251.A.9A4.html'
    )))
<commit_msg>Fix for the deleted entry<commit_after>#!/usr/bin/env python

import logging
from os import mkdir
from os.path import join as path_join
from urllib.parse import quote_plus, urljoin
from datetime import datetime

import requests
from bs4 import BeautifulSoup

logging.basicConfig(
    format=(
        '%(asctime)s\t%(levelname)s\t'
        #'%(processName)s\t%(threadName)s\t'
        '%(name)s\t%(funcName)s:%(lineno)d\t'
        '%(message)s'
    ),
    level=logging.DEBUG
)

def _make_fake_browser():

    fake_browser = requests.Session()
    fake_browser.headers = {
        'user-agent': (
            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) '
            'AppleWebKit/537.36 (KHTML, like Gecko) '
            'Chrome/54.0.2840.98 Safari/537.36'
        ),
        'accept': (
            'text/html,application/xhtml+xml,application/xml;q=0.9,'
            'image/webp,*/*;q=0.8'
        ),
        'accept-encoding': 'gzip, deflate, sdch, br',
        'accept-language': 'en-US,en;q=0.8,zh-TW;q=0.6,zh;q=0.4',
        'cookie': 'over18=1',
    }

    return fake_browser

_SHARED_FAKE_BROWSER = _make_fake_browser()
_CACHE_DIR_PATH = 'cache/'
_URL_SET_SKIPPING_CACHE = {'https://www.ptt.cc/bbs/Gossiping/index.html'}

def read_or_request(url):

    # should generate valid fname for most of the systems
    fname = quote_plus(url)
    path = path_join(_CACHE_DIR_PATH, fname)

    # try cache

    if url in _URL_SET_SKIPPING_CACHE:

        logging.info('Skip cache for {}'.format(url))

    else:

        try:
            with open(path) as f:
                logging.info('Hit {}'.format(url))
                return f.read()
        except OSError:
            logging.info('Missed {}'.format(url))

    # request

    resp = _SHARED_FAKE_BROWSER.get(url)
    text = resp.text

    while 1:

        try:
            with open(path, 'w') as f:
                f.write(text)
        except FileNotFoundError:
            mkdir(_CACHE_DIR_PATH)
            continue
        else:
            logging.info('Wrote {}'.format(url))

        # only loop once normally
        break

    return text

_ROOT = 'https://www.ptt.cc'

def parse_index_page(text):

    soup = BeautifulSoup(text, 'html.parser')

    # paging

    paging_a_tags = soup.select('.action-bar .btn-group-paging a')

    prev_a_tag = paging_a_tags[1]
    next_a_tag = paging_a_tags[2]

    if '上頁' not in prev_a_tag.string:
        raise RuntimeError('the prev button looks not right')
    if '下頁' not in next_a_tag.string:
        raise RuntimeError('the next button looks not right')

    prev_url = urljoin(_ROOT, prev_a_tag.get('href', ''))
    next_url = urljoin(_ROOT, next_a_tag.get('href', ''))

    # entries

    entry_ds = []
    ent_tags = soup.select('.r-list-container .r-ent')
    for ent_tag in ent_tags:

        # push_count

        # cases
        #
        # 1.
12

        # 2.

        # 3.
X1

        #
        push_count = -65535
        span_tag = ent_tag.find(class_='nrec').find('span')

        # if case 2
        if not span_tag:

            push_count = 0

        else:

            span_text = span_tag.string

            while 1:

                # if case 1
                try:
                    push_count = int(span_text)
                except ValueError:
                    pass
                else:
                    break

                # if case 3
                try:
                    push_count = int(span_text[1:])
                except ValueError:
                    pass
                else:
                    push_count = -(push_count+10)
                    break

                # loop once normally
                break

        # title & article_url

        # cases
        #
        # 1.

        #
        #            Re: [問卦] 有沒有高雄其實比台北進步的八卦??
        #
        #

        # 2.
 (本文已被刪除) [gundam0613]

        #

        title_a_tag = ent_tag.find(class_='title').a
        if not title_a_tag:
            continue

        title = title_a_tag.string
        article_url = path_join(_ROOT, title_a_tag.get('href', ''))

        # others

        ent_meta_tag = ent_tag.find(class_='meta')

        # mmdd won't contain year!
        raw_mmdd = ent_meta_tag.find(class_='date').string
        author_id = ent_meta_tag.find(class_='author').string

        # append

        entry_ds.append({
            'push_count' : push_count,
            'title'      : title,
            'article_url': article_url,
            'raw_mmdd'   : raw_mmdd,
            'author_id'  : author_id
        })

    return {
        'prev_url': prev_url,
        'next_url': next_url,
        'entry_ds': entry_ds
    }

_EXPECTED_ARTICLE_TAG_KEYS = ('作者', '看板', '標題', '時間')
_PUSH_TAG_STRIPPED_TEXT_SCORE_MAP = {
    '噓': -1,
    '→': 0,
    '推': 1
}

def parse_article_page(text):

    soup = BeautifulSoup(text, 'html.parser')
    main_content_tag = soup.find(id='main-content')

    # meta
    #
    #

    #     作者
    #     a77774444 (我愛ˋ台灣)
    #

    #
    for tag, key in zip(
        main_content_tag.select('.article-meta-tag'),
        _EXPECTED_ARTICLE_TAG_KEYS
    ):
        if tag.string != key:
            raise RuntimeError('the article tags may be changed')

    author_line, board_name, title, timestamp_text = (
        tag.string
        for tag in main_content_tag.select('.article-meta-value')
    )

    author_id, _, author_line_tail = author_line.partition(' (')
    author_nick = author_line_tail[:-1]

    # Tue Nov 29 05:05:03 2016
    # Fri Jul  1 21:12:25 2005
    created_dt = datetime.strptime(timestamp_text, '%a %b %d %H:%M:%S %Y')

    # body

    recording = False
    body_lines = []
    for i, line in enumerate(main_content_tag.stripped_strings):

        if i == 6:
            if line != '時間':
                raise RuntimeError('the article structure may be changed')
        elif i == 8:
            recording = True
        elif line.startswith('※ 發信站: 批踢踢實業坊'):
            recording = False
            break

        if recording:
            body_lines.append(line)

    else:

        raise RuntimeError('the ending text of article may be change/')

    # ... 以後台北人都要去高雄當台勞了。\n\n--'
    body = '\n'.join(body_lines).rstrip('\n-')

    # pushes

    push_ds = [
        {
            'score': _PUSH_TAG_STRIPPED_TEXT_SCORE_MAP.get(
                push_tag.find(class_='push-tag').string.strip(), -255
            ),
            'id': push_tag.find(class_='push-userid').string,
            'text': push_tag.find(class_='push-content').string,
            # won't contain year!
            'raw_mmddhhmm': push_tag.find(class_='push-ipdatetime').string.strip()
        }
        for push_tag in main_content_tag.select('.push')
    ]

    return {
        'board_name' : board_name,
        'author_id'  : author_id,
        'author_nick': author_nick,
        'title'      : title,
        'created_dt' : created_dt,
        'body'       : body,
        'push_ds'    : push_ds
    }

if __name__ == '__main__':

    from pprint import pprint

    # test the index page
    pprint(parse_index_page(read_or_request(
        'https://www.ptt.cc/bbs/Gossiping/index20177.html'
    )))

    # test the article page #1
    pprint(parse_article_page(read_or_request(
        'https://www.ptt.cc/bbs/Gossiping/M.1480367106.A.A55.html'
    )))

    # test the article page #2
    pprint(parse_article_page(read_or_request(
        'https://www.ptt.cc/bbs/Gossiping/M.1480380251.A.9A4.html'
    )))
<|endoftext|>


<commit_before># encoding: utf8
"""
This module encapsulates useful classes and data for calculating with SI quantities.
Loosely based on the `English translation of the SI brochure`__.

.. __: http://www.bipm.org/utils/common/pdf/si_brochure_8_en.pdf

>>> from si.common import *
>>> print (1*bar)*(1*l)
100 J

You can also extract additional information about units:

>>> from si.register import search
>>> search("m").description
'SI base unit of length'

Uses floats and python math by default. Override by setting SI.math to a math emulating module, SI.nonint to (a wrapper around) your numeric type, truediv to a good divsion function, and pow (see default function).

Caveats:

	* SI objects are purely mathematical (no physical concepts) and therefore can not have any notion of what they are *really* representing. Don't expect them to make a difference between Nm and J!
	* Python/IEEE floats are not ideal to represent negative powers of ten. Take the difference between mL and cm3 to know what I mean.
	* When using SI units in connection with other objects emulating mathematical behavior, you will have to take care of the sequence when multiplying, although SI objects themselves are commutative. For example, until `a bug in sympy`__ is fixed, you have to write ``kg*sympify("1/10")`` instead of ``sympify("1/10")*kg``.

.. __: http://code.google.com/p/sympy/issues/detail?id=432
"""
from __future__ import division
import sys
import warnings

import si.register
import si.math

class MakesNoSense(Exception):
	"""Exception raised when impossible operations are attempted on SI objects.

	>>> from si.common import *
	>>> m + s
	Traceback (most recent call last):
	  ...
	MakesNoSense: Adding non-compatible quantities.
	"""

class Exponents(dict):
	"""Dictionary mapping symbolic bases to exponents."""
	def _posneg(self):
		pos=[x for x in self.items() if x[1]>0]
		neg=[(x[0],-x[1]) for x in self.items() if x[1]<0]
		pos.sort(lambda x,y:cmp(x[1],y[1]))
		neg.sort(lambda x,y:cmp(x[1],y[1]))
		return pos, neg
	def __str__(self):
		"""Standard string representation.

		>>> print Exponents({'m':1,'s':-2})
		m/s^2
		>>> print Exponents({'s':-1})
		/s
		>>> print Exponents({'a':1,'b':-2,'c':3,'d':-10})
		a c^3/(b^2 d^10)
		"""
		pos, neg = self._posneg()
		pospart,negpart=[],[]
		for part,d in [(pospart,pos),(negpart,neg)]:
			for s,n in d:
				part.append(unicode(s)+(n!=1 and "^%s"%n or ""))
		r=" ".join(pospart)
		if negpart:
			r+='/'
			if len(negpart)>1: r+='('
			r+=" ".join(negpart)
			if len(negpart)>1: r+=')'
		return r.strip()

	def tex(self, use_over = False):
		"""TeX math mode representation.

		>>> print Exponents({'m':1,'s':-2}).tex()
		m/s^2
		>>> print Exponents({'s':-1}).tex(use_over = True)
		{1}\over{s}
		"""
		pos, neg = self._posneg()
		pospart,negpart=[],[]
		for part,d in [(pospart,pos),(negpart,neg)]:
			for s,n in d:
				if n==1:
					part.append(s)
				else:
					n = str(n)
					if len(n)>1: n="{%s}"%n
					part.append(unicode(s)+"^"+n)
		if use_over and negpart:
			pospart = pospart or ["1"]
			return r"{%s}\over{%s}"%(" ".join(pospart)," ".join(negpart))
		else:
			r = " ".join(pospart)
			if negpart:
				r += "/"
				if len(negpart)>1: r+= r"\left("
				r += " ".join(negpart)
				if len(negpart)>1: r+= r"\right)"
		return r.strip()

class SI(tuple):
	"""SI quantity, storeed as a 2-tuple of value and a tuple storing the dimension (α to η)."""

	symbols=('m','kg','s','A','K','mol','cd') # for simple representation

	value=property(lambda self:self[0])
	dim=property(lambda self:self[1])
	unit=property(lambda self:SI((1,self.dim)))

	@staticmethod
	def _exponents_mul(A,B):
		return tuple([a+b for a,b in zip(A,B)])
	@staticmethod
	def _exponents_div(A,B):
		return tuple([a-b for a,b in zip(A,B)])

	def __add__(self,other):
		""">>> from si.units.common import *
		>>> print 1*m+2*m
		3 m"""
		if self.dim!=other.dim: raise MakesNoSense("Adding non-compatible quantities.")
		return SI((self.value+other.value,self.dim))
	def __sub__(self,other):
		""">>> from si.units.common import *
		>>> print (m+m)-m
		1 m"""
		if self.dim!=other.dim: raise MakesNoSense("Subtracting non-compatible quantities.")
		return SI((self.value-other.value,self.dim))
	def __mul__(self,other):
		""">>> from si.units.common import *
		>>> print 2*m
		2 m
		>>> print m*2
		2 m
		>>> print 2*m*m
		2 m^2
		>>> print 1/s * s
		1"""
		if not hasattr(other,'dim'):
			return SI((self.value*other,self.dim))
		newexp = self._exponents_mul(self.dim,other.dim)
		if not self._expsum(newexp):
			return self.value * other.value
		return SI((self.value*other.value,newexp))
	__rmul__=__mul__
	def __div__(self,other):
		""">>> from si.units.common import *
		>>> print (m*m)/m
		1 m
		>>> print (10*m)/2
		5 m
		>>> print (10*m)/(5*m)
		2"""
		if not hasattr(other,'dim'):
			return SI((si.math.truediv(self.value,other),self.dim))
		newexp = self._exponents_div(self.dim,other.dim)
		if not self._expsum(newexp):
			return si.math.truediv(self.value,other.value)
		return SI((si.math.truediv(self.value,other.value),newexp))
	__truediv__=__div__
	def __rdiv__(self,other):
		""">>> from si.units.common import *
		>>> print 5/Hz
		5 s"""
		if hasattr(other,'dim'):
			raise NotImplementedError("SI units can handle divisions themselves, no need to do this here")
		return SI((si.math.truediv(other,self.value),tuple(-x for x in self.dim)))
	__rtruediv__=__rdiv__
	def __pow__(self,exp):
		""">>> from si.units.common import *
		>>> print (5*m)**2
		25 m^2"""
		return SI((si.math.pow(self.value,exp),tuple(a*exp for a in self.dim)))

	def __cmp__(self,other):
		if not hasattr(other, "dim") or self.dim!=other.dim:
			raise MakesNoSense("Comparing non-compatible quantities.")
		return cmp(self[0],other[0])
	def __nonzero__(self):
		return bool(self.value)

	def __lt__(self, other):
		""">>> from si.units.common import *
		>>> 1*m < 5*s
		Traceback (most recent call last):
		  ...
		MakesNoSense: Comparing non-compatible quantities.
		"""
		return self.__cmp__(other)<0
	def __gt__(self, other):
		""">>> from si.units.common import *
		>>> 1*m > 1
		Traceback (most recent call last):
		  ...
		MakesNoSense: Comparing non-compatible quantities.
		"""
		return self.__cmp__(other)>0
	def __le__(self, other):
		return self.__cmp__(other)<=0
	def __ge__(self, other):
		return self.__cmp__(other)>=0

	def using(self,unit):
		"""Get numeric value in given unit.
		Use this instead of division if you want to make sure that the units match. (Otherwise, an exception is raised.)

		How much is 1 m/s in attorparsec per microfortnight?

		>>> from si.units.exotic import *
		>>> print (1*m/s).using(apc/ufortnight) # doctest: +ELLIPSIS
		39.2004662878...
		"""
		if self.dim!=unit.dim: raise MakesNoSense("The quantity can not be expressed in that unit.")
		return self.value/unit.value

	def basestring(self):
		"""String representation using only powers of base units."""
		return "%s %s"%(self.value,Exponents(zip(self.symbols,self.dim)))
	def __repr__(self):
		return '<SI %s>'%self.basestring()

	@staticmethod
	def _expsum(u):
		return sum([abs(x) for x in u])

	def _decomposition(self):
		u_exact = []
		u_always = []

		for m in si.register.ModuleSIRegister.loadedmodules:
			for ru in m.units:
				if ru.map != "never": u_exact.append(ru)
				if ru.map == "always": u_always.append(ru)

		u_always.sort(cmp = lambda a,b: -cmp(self._expsum(a.unit.dim), self._expsum(b.unit.dim)))

		exactmatch = []
		for ru in u_exact:
			if ru.unit == self.unit:
				exactmatch.append(ru)

		if exactmatch:
			if len(exactmatch)>1:
				warnings.warn("More than one registered unit matches this quantity: %s."%exactmatch)
			decomposition = {exactmatch[0]:1}
			remaining = self / exactmatch[0].unit
		else:
			decomposition = {}
			remaining = self
			last = None

			while hasattr(remaining, "dim") and (last is None or last != remaining):
				last = remaining
				currentexpsum=self._expsum(remaining.dim)
				for ru in u_always:
					div = remaining / ru.unit
					if not hasattr(div, "dim") or ( # we are through with this
								not sum([abs(a) < abs(b) for a,b in zip(remaining.dim, div.dim)]) # don't use factors that enlarge exponents
								and
								not sum([abs(a) < abs(b) for a,b in zip(remaining.dim, ru.unit.dim)]) # don't use factors larger than remaining
							) and self._expsum(div.dim)<currentexpsum: # "pays off"
						decomposition[ru] = decomposition.get(ru,0) + 1
						remaining=div
						break

			# now for negative exponents

			last = None
			while hasattr(remaining, "dim") and (last is None or last != remaining): # copy/pasted from above, just changed operators. FIXME: enhance
				last = remaining
				currentexpsum=self._expsum(remaining.dim)
				for ru in u_always:
					div = remaining * ru.unit
					if not hasattr(div, "dim") or ( # we are through with this
								not sum([abs(a) < abs(b) for a,b in zip(remaining.dim, div.dim)]) # don't use factors that enlarge exponents
								and
								not sum([abs(a) < abs(b) for a,b in zip(remaining.dim, ru.unit.dim)]) # don't use factors larger than remaining
							) and self._expsum(div.dim)<currentexpsum: # "pays off"
						decomposition[ru] = decomposition.get(ru,0) - 1
						remaining=div
						break

			assert not hasattr(remaining, "dim"), "Didn't catch all exponents. Base units are probably not registered!"

		remaining = math.simplest_form(remaining)
		return (remaining, decomposition)

	def intelligentstring(self, unicode = True):
		"""Return a string representation using compound SI units of already loaded modules. The function will roughly try to:

		- minimize the sum of absolute values of exponents:
		>>> from si.units.common import *
		>>> gravity=10*m/s/s; height=5*m; mass=5*kg
		>>> print gravity*height*mass
		250 J

		- avoid units with no positive exponents:
		>>> print 5/s
		5 Hz

		Will use unicode strings unless unicode is false.
		"""
		remaining, decomposition = self._decomposition()

		return "%s %s"%(remaining, Exponents([(unit.preferred_symbol(unicode), power) for (unit, power) in decomposition.iteritems()]))

	def tex(self, use_over = False):
		"""Return a TeX math mode representation, using the same logic as ``intelligentstring``.

		>>> from si.common import *
		>>> print (5*m/s).tex(use_over=True)
		5 {m}\\over{s}
		"""
		remaining, decomposition = self._decomposition()

		if hasattr(remaining, "tex"):
			remaining = remaining.tex() # if it looks like a ghost from the future and talks like a ghost from the future, assume it is a ghost from the future. (pro-active ducktyping)

		return "%s %s"%(remaining, Exponents([(unit.tex(), power) for (unit, power) in decomposition.iteritems()]).tex(use_over = use_over))

	def __unicode__(self): return self.intelligentstring(True)

	def __str__(self): return self.intelligentstring(False)

<commit_msg>fixed .tex(use_over=True) for correct brackets<commit_after># encoding: utf8
"""
This module encapsulates useful classes and data for calculating with SI quantities.
Loosely based on the `English translation of the SI brochure`__.

.. __: http://www.bipm.org/utils/common/pdf/si_brochure_8_en.pdf

>>> from si.common import *
>>> print (1*bar)*(1*l)
100 J

You can also extract additional information about units:

>>> from si.register import search
>>> search("m").description
'SI base unit of length'

Uses floats and python math by default. Override by setting SI.math to a math emulating module, SI.nonint to (a wrapper around) your numeric type, truediv to a good divsion function, and pow (see default function).

Caveats:

	* SI objects are purely mathematical (no physical concepts) and therefore can not have any notion of what they are *really* representing. Don't expect them to make a difference between Nm and J!
	* Python/IEEE floats are not ideal to represent negative powers of ten. Take the difference between mL and cm3 to know what I mean.
	* When using SI units in connection with other objects emulating mathematical behavior, you will have to take care of the sequence when multiplying, although SI objects themselves are commutative. For example, until `a bug in sympy`__ is fixed, you have to write ``kg*sympify("1/10")`` instead of ``sympify("1/10")*kg``.

.. __: http://code.google.com/p/sympy/issues/detail?id=432
"""
from __future__ import division
import sys
import warnings

import si.register
import si.math

class MakesNoSense(Exception):
	"""Exception raised when impossible operations are attempted on SI objects.

	>>> from si.common import *
	>>> m + s
	Traceback (most recent call last):
	  ...
	MakesNoSense: Adding non-compatible quantities.
	"""

class Exponents(dict):
	"""Dictionary mapping symbolic bases to exponents."""
	def _posneg(self):
		pos=[x for x in self.items() if x[1]>0]
		neg=[(x[0],-x[1]) for x in self.items() if x[1]<0]
		pos.sort(lambda x,y:cmp(x[1],y[1]))
		neg.sort(lambda x,y:cmp(x[1],y[1]))
		return pos, neg
	def __str__(self):
		"""Standard string representation.

		>>> print Exponents({'m':1,'s':-2})
		m/s^2
		>>> print Exponents({'s':-1})
		/s
		>>> print Exponents({'a':1,'b':-2,'c':3,'d':-10})
		a c^3/(b^2 d^10)
		"""
		pos, neg = self._posneg()
		pospart,negpart=[],[]
		for part,d in [(pospart,pos),(negpart,neg)]:
			for s,n in d:
				part.append(unicode(s)+(n!=1 and "^%s"%n or ""))
		r=" ".join(pospart)
		if negpart:
			r+='/'
			if len(negpart)>1: r+='('
			r+=" ".join(negpart)
			if len(negpart)>1: r+=')'
		return r.strip()

	def tex(self, use_over = False):
		"""TeX math mode representation.

		>>> print Exponents({'m':1,'s':-2}).tex()
		m/s^2
		>>> print Exponents({'s':-1}).tex(use_over = True)
		{1 \\over s}
		"""
		pos, neg = self._posneg()
		pospart,negpart=[],[]
		for part,d in [(pospart,pos),(negpart,neg)]:
			for s,n in d:
				if n==1:
					part.append(s)
				else:
					n = str(n)
					if len(n)>1: n="{%s}"%n
					part.append(unicode(s)+"^"+n)
		if use_over and negpart:
			pospart = pospart or ["1"]
			return r"{%s \over %s}"%(" ".join(pospart)," ".join(negpart))
		else:
			r = " ".join(pospart)
			if negpart:
				r += "/"
				if len(negpart)>1: r+= r"\left("
				r += " ".join(negpart)
				if len(negpart)>1: r+= r"\right)"
		return r.strip()

class SI(tuple):
	"""SI quantity, storeed as a 2-tuple of value and a tuple storing the dimension (α to η)."""

	symbols=('m','kg','s','A','K','mol','cd') # for simple representation

	value=property(lambda self:self[0])
	dim=property(lambda self:self[1])
	unit=property(lambda self:SI((1,self.dim)))

	@staticmethod
	def _exponents_mul(A,B):
		return tuple([a+b for a,b in zip(A,B)])
	@staticmethod
	def _exponents_div(A,B):
		return tuple([a-b for a,b in zip(A,B)])

	def __add__(self,other):
		""">>> from si.units.common import *
		>>> print 1*m+2*m
		3 m"""
		if self.dim!=other.dim: raise MakesNoSense("Adding non-compatible quantities.")
		return SI((self.value+other.value,self.dim))
	def __sub__(self,other):
		""">>> from si.units.common import *
		>>> print (m+m)-m
		1 m"""
		if self.dim!=other.dim: raise MakesNoSense("Subtracting non-compatible quantities.")
		return SI((self.value-other.value,self.dim))
	def __mul__(self,other):
		""">>> from si.units.common import *
		>>> print 2*m
		2 m
		>>> print m*2
		2 m
		>>> print 2*m*m
		2 m^2
		>>> print 1/s * s
		1"""
		if not hasattr(other,'dim'):
			return SI((self.value*other,self.dim))
		newexp = self._exponents_mul(self.dim,other.dim)
		if not self._expsum(newexp):
			return self.value * other.value
		return SI((self.value*other.value,newexp))
	__rmul__=__mul__
	def __div__(self,other):
		""">>> from si.units.common import *
		>>> print (m*m)/m
		1 m
		>>> print (10*m)/2
		5 m
		>>> print (10*m)/(5*m)
		2"""
		if not hasattr(other,'dim'):
			return SI((si.math.truediv(self.value,other),self.dim))
		newexp = self._exponents_div(self.dim,other.dim)
		if not self._expsum(newexp):
			return si.math.truediv(self.value,other.value)
		return SI((si.math.truediv(self.value,other.value),newexp))
	__truediv__=__div__
	def __rdiv__(self,other):
		""">>> from si.units.common import *
		>>> print 5/Hz
		5 s"""
		if hasattr(other,'dim'):
			raise NotImplementedError("SI units can handle divisions themselves, no need to do this here")
		return SI((si.math.truediv(other,self.value),tuple(-x for x in self.dim)))
	__rtruediv__=__rdiv__
	def __pow__(self,exp):
		""">>> from si.units.common import *
		>>> print (5*m)**2
		25 m^2"""
		return SI((si.math.pow(self.value,exp),tuple(a*exp for a in self.dim)))

	def __cmp__(self,other):
		if not hasattr(other, "dim") or self.dim!=other.dim:
			raise MakesNoSense("Comparing non-compatible quantities.")
		return cmp(self[0],other[0])
	def __nonzero__(self):
		return bool(self.value)

	def __lt__(self, other):
		""">>> from si.units.common import *
		>>> 1*m < 5*s
		Traceback (most recent call last):
		  ...
		MakesNoSense: Comparing non-compatible quantities.
		"""
		return self.__cmp__(other)<0
	def __gt__(self, other):
		""">>> from si.units.common import *
		>>> 1*m > 1
		Traceback (most recent call last):
		  ...
		MakesNoSense: Comparing non-compatible quantities.
		"""
		return self.__cmp__(other)>0
	def __le__(self, other):
		return self.__cmp__(other)<=0
	def __ge__(self, other):
		return self.__cmp__(other)>=0

	def using(self,unit):
		"""Get numeric value in given unit.
		Use this instead of division if you want to make sure that the units match. (Otherwise, an exception is raised.)

		How much is 1 m/s in attorparsec per microfortnight?

		>>> from si.units.exotic import *
		>>> print (1*m/s).using(apc/ufortnight) # doctest: +ELLIPSIS
		39.2004662878...
		"""
		if self.dim!=unit.dim: raise MakesNoSense("The quantity can not be expressed in that unit.")
		return self.value/unit.value

	def basestring(self):
		"""String representation using only powers of base units."""
		return "%s %s"%(self.value,Exponents(zip(self.symbols,self.dim)))
	def __repr__(self):
		return '<SI %s>'%self.basestring()

	@staticmethod
	def _expsum(u):
		return sum([abs(x) for x in u])

	def _decomposition(self):
		u_exact = []
		u_always = []

		for m in si.register.ModuleSIRegister.loadedmodules:
			for ru in m.units:
				if ru.map != "never": u_exact.append(ru)
				if ru.map == "always": u_always.append(ru)

		u_always.sort(cmp = lambda a,b: -cmp(self._expsum(a.unit.dim), self._expsum(b.unit.dim)))

		exactmatch = []
		for ru in u_exact:
			if ru.unit == self.unit:
				exactmatch.append(ru)

		if exactmatch:
			if len(exactmatch)>1:
				warnings.warn("More than one registered unit matches this quantity: %s."%exactmatch)
			decomposition = {exactmatch[0]:1}
			remaining = self / exactmatch[0].unit
		else:
			decomposition = {}
			remaining = self
			last = None

			while hasattr(remaining, "dim") and (last is None or last != remaining):
				last = remaining
				currentexpsum=self._expsum(remaining.dim)
				for ru in u_always:
					div = remaining / ru.unit
					if not hasattr(div, "dim") or ( # we are through with this
								not sum([abs(a) < abs(b) for a,b in zip(remaining.dim, div.dim)]) # don't use factors that enlarge exponents
								and
								not sum([abs(a) < abs(b) for a,b in zip(remaining.dim, ru.unit.dim)]) # don't use factors larger than remaining
							) and self._expsum(div.dim)<currentexpsum: # "pays off"
						decomposition[ru] = decomposition.get(ru,0) + 1
						remaining=div
						break

			# now for negative exponents

			last = None
			while hasattr(remaining, "dim") and (last is None or last != remaining): # copy/pasted from above, just changed operators. FIXME: enhance
				last = remaining
				currentexpsum=self._expsum(remaining.dim)
				for ru in u_always:
					div = remaining * ru.unit
					if not hasattr(div, "dim") or ( # we are through with this
								not sum([abs(a) < abs(b) for a,b in zip(remaining.dim, div.dim)]) # don't use factors that enlarge exponents
								and
								not sum([abs(a) < abs(b) for a,b in zip(remaining.dim, ru.unit.dim)]) # don't use factors larger than remaining
							) and self._expsum(div.dim)<currentexpsum: # "pays off"
						decomposition[ru] = decomposition.get(ru,0) - 1
						remaining=div
						break

			assert not hasattr(remaining, "dim"), "Didn't catch all exponents. Base units are probably not registered!"

		remaining = math.simplest_form(remaining)
		return (remaining, decomposition)

	def intelligentstring(self, unicode = True):
		"""Return a string representation using compound SI units of already loaded modules. The function will roughly try to:

		- minimize the sum of absolute values of exponents:
		>>> from si.units.common import *
		>>> gravity=10*m/s/s; height=5*m; mass=5*kg
		>>> print gravity*height*mass
		250 J

		- avoid units with no positive exponents:
		>>> print 5/s
		5 Hz

		Will use unicode strings unless unicode is false.
		"""
		remaining, decomposition = self._decomposition()

		return "%s %s"%(remaining, Exponents([(unit.preferred_symbol(unicode), power) for (unit, power) in decomposition.iteritems()]))

	def tex(self, use_over = False):
		"""Return a TeX math mode representation, using the same logic as ``intelligentstring``.

		>>> from si.common import *
		>>> print (5*m/s).tex(use_over=True)
		5 {m \\over s}
		"""
		remaining, decomposition = self._decomposition()

		if hasattr(remaining, "tex"):
			remaining = remaining.tex() # if it looks like a ghost from the future and talks like a ghost from the future, assume it is a ghost from the future. (pro-active ducktyping)

		return "%s %s"%(remaining, Exponents([(unit.tex(), power) for (unit, power) in decomposition.iteritems()]).tex(use_over = use_over))

	def __unicode__(self): return self.intelligentstring(True)

	def __str__(self): return self.intelligentstring(False)

<|endoftext|>