<commit_before># -*- coding: utf-8 -*-

from decimal import Decimal
from django.core.urlresolvers import reverse
from django.db import models
from django_countries.fields import CountryField
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill
from Instanssi.kompomaatti.models import Event
from common.misc import get_url

class StoreItem(models.Model):
    event = models.ForeignKey(
        Event,
        verbose_name=u'Tapahtuma',
        help_text=u'Tapahtuma johon tuote liittyy.',
        blank=True,
        null=True)
    name = models.CharField(
        u'Tuotteen nimi',
        help_text=u'Tuotteen lyhyt nimi.',
        max_length=255)
    description = models.TextField(
        u'Tuotteen kuvaus',
        help_text=u'Tuotteen pitkä kuvaus.')
    price = models.DecimalField(
        u'Tuotteen hinta',
        help_text=u'Tuotteen hinta.',
        max_digits=5,
        decimal_places=2)
    max = models.IntegerField(
        u'Kappaletta saatavilla',
        help_text=u'Kuinka monta kappaletta on ostettavissa ennen myynnin lopettamista.')
    available = models.BooleanField(
        u'Ostettavissa',
        default=False,
        help_text=u'Ilmoittaa, näkyykö tuote kaupassa.')
    imagefile_original = models.ImageField(
        u'Tuotekuva',
        upload_to='store/images/',
        help_text=u"Edustava kuva tuotteelle.", blank=True, null=True)
    imagefile_thumbnail = ImageSpecField(
        [ResizeToFill(64, 64)],
        source='imagefile_original',
        format='PNG')
    max_per_order = models.IntegerField(
        u'Maksimi per tilaus',
        default=10,
        help_text=u'Kuinka monta kappaletta voidaan ostaa kerralla.')
    sort_index = models.IntegerField(
        u'Järjestysarvo',
        default=0,
        help_text=u'Tuotteet esitetään kaupassa tämän luvun mukaan järjestettynä, pienempilukuiset ensin.')
    discount_amount = models.IntegerField(
        u'Alennusmäärä',
        default=-1,
        help_text=u'Pienin määrä tuotteita joka oikeuttaa alennukseen (-1 = ei mitään)')
    discount_percentage = models.IntegerField(
        u'Alennusprosentti',
        default=0,
        help_text=u'Alennuksen määrä prosentteina kun tuotteiden määrä saavuttaa alennusmäärän.')

    def is_discount_available(self):
        return self.discount_amount >= 0

    def get_discount_factor(self):
        return (100.0 - self.discount_percentage) / 100.0

    def get_discounted_unit_price(self, amount):
        """Returns decimal price of item considering any quantity discount."""
        if amount >= self.discount_amount and amount >= 0:
            factor = (Decimal(100) - Decimal(self.discount_percentage)) / 100
            price = self.price * factor
        else:
            price = self.price
        # If we need to consider rounding direction, this might be the place.
        # Pass "rounding=METHOD" as second argument.
        return price.quantize(Decimal('0.01'))

    def get_discounted_subtotal(self, amount):
        """Returns decimal subtotal for a specific number of items, considering
        any quantity discount."""
        return self.get_discounted_unit_price(amount) * amount

    def num_available(self):
        return min(self.max - self.num_sold(), self.max_per_order)

    def num_in_store(self):
        return self.max - self.num_sold()

    def num_sold(self):
        return TransactionItem.objects.filter(
            transaction__time_paid__isnull=False,
            item=self
        ).count()

    @staticmethod
    def items_available():
        return StoreItem.objects.filter(max__gt=0, available=True).order_by('sort_index')

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = u"tuote"
        verbose_name_plural = u"tuotteet"

class StoreTransaction(models.Model):
    token = models.CharField(
        u'Palvelutunniste',
        help_text=u'Maksupalvelun maksukohtainen tunniste',
        max_length=255)
    time_created = models.DateTimeField(
        u'Luontiaika',
        null=True,
        blank=True)
    time_paid = models.DateTimeField(
        u'Maksun varmistumisaika',
        null=True,
        blank=True)
    time_pending = models.DateTimeField(
        u'Maksun maksuaika',
        null=True,
        blank=True)
    time_cancelled = models.DateTimeField(
        u'Peruutusaika',
        null=True,
        blank=True)
    payment_method_name = models.CharField(
        u'Maksutapa',
        help_text=u'Tapa jolla tilaus maksettiin',
        max_length=32,
        blank=True,
        default=u'')
    key = models.CharField(
        u'Avain',
        max_length=40,
        unique=True,
        help_text=u'Paikallinen maksukohtainen tunniste')
    firstname = models.CharField(
        u'Etunimi',
        max_length=64)
    lastname = models.CharField(
        u'Sukunimi',
        max_length=64)
    company = models.CharField\
        (u'Yritys',
         max_length=128,
         blank=True)
    email = models.EmailField(
        u'Sähköposti',
        max_length=255,
        help_text=u'Sähköpostiosoitteen on oltava toimiva, sillä liput ja tuotteiden lunastukseen '
                  u'tarvittavat koodit lähetetään sinne.')
    telephone = models.CharField(
        u'Puhelinnumero',
        max_length=64,
        blank=True)
    mobile = models.CharField(
        u'Matkapuhelin',
        max_length=64,
        blank=True)
    street = models.CharField(
        u'Katuosoite',
        max_length=128,
        help_text=u'Katusoite tarvitaan maksupalvelun vaatimuksesta.')
    postalcode = models.CharField(
        u'Postinumero',
        max_length=16)
    city = models.CharField(
        u'Postitoimipaikka',
        max_length=64)
    country = CountryField(
        u'Maa',
        default='FI')
    information = models.TextField(
        u'Lisätiedot',
        help_text=u'Mikäli tilaukseen kuuluu T-paitoja, määritä niiden koot tässä.',
        blank=True)

    @property
    def is_paid(self):
        return self.time_paid is not None

    @property
    def is_cancelled(self):
        return self.time_cancelled is not None

    @property
    def is_pending(self):
        return self.time_pending is not None

    @property
    def is_delivered(self):
        for item in self.get_items():
            if not item.is_delivered:
                return False
        return True

    @property
    def qr_code(self):
        return get_url(reverse('store:ta_view', kwargs={'transaction_key': self.key}))

    @property
    def full_name(self):
        return u'{} {}'.format(self.firstname, self.lastname)

    def get_status_text(self):
        if self.is_cancelled:
            return u'Peruutettu'
        if self.is_delivered:
            return u'Toimitettu'
        if self.is_paid:
            return u'Maksettu'
        if self.is_pending:
            return u'Vireillä'
        return u'Tuotteet valittu'

    def get_total_price(self):
        ret = 0
        for item in TransactionItem.objects.filter(transaction=self):
            ret += item.purchase_price
        return ret

    def get_transaction_items(self):
        return TransactionItem.objects.filter(transaction=self)

    def get_distinct_storeitems_and_prices(self):
        """Returns a list of unique (StoreItem, price) tuples related to
        this transaction."""
        # get (item-id, price) tuples
        items = self.get_transaction_items().values_list(
            'item', 'purchase_price').distinct()
        itemlist = []
        for key, price in items:
            # this isn't a performance issue with our database
            itemlist.append((StoreItem.objects.filter(pk=key).get(), price))
        return itemlist

    def get_storeitem_count(self, store_item):
        return TransactionItem.objects.filter(
            item=store_item, transaction=self).count()

    def __unicode__(self):
        return self.full_name

    class Meta:
        verbose_name = u"transaktio"
        verbose_name_plural = u"transaktiot"
        permissions = (("view_storetransaction", "Can view store transactions"),)

class TransactionItem(models.Model):
    key = models.CharField(
        u'Avain',
        max_length=40,
        unique=True,
        help_text=u'Lippuavain')
    item = models.ForeignKey(
        StoreItem,
        verbose_name=u'Tuote')
    transaction = models.ForeignKey(
        StoreTransaction,
        verbose_name=u'Ostotapahtuma')
    time_delivered = models.DateTimeField(
        u'Toimitusaika',
        null=True,
        blank=True)
    purchase_price = models.DecimalField(
        u'Tuotteen hinta',
        help_text=u'Tuotteen hinta ostoshetkellä',
        max_digits=5,
        decimal_places=2)
    original_price = models.DecimalField(
        u'Tuotteen alkuperäinen hinta',
        help_text=u'Tuotteen hinta ostoshetkellä ilman alennuksia',
        max_digits=5,
        decimal_places=2)

    @property
    def is_delivered(self):
        return self.time_delivered is not None

    @property
    def qr_code(self):
        return get_url(reverse('store:ti_view', kwargs={'item_key': self.key}))

    def __unicode__(self):
        return u'{} for {}'.format(self.item.name, self.transaction.full_name)

    class Meta:
        verbose_name = u"transaktiotuote"
        verbose_name_plural = u"transaktiotuotteet"
<commit_msg>store: Fix error in admin status page<commit_after># -*- coding: utf-8 -*-

from decimal import Decimal
from django.core.urlresolvers import reverse
from django.db import models
from django_countries.fields import CountryField
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill
from Instanssi.kompomaatti.models import Event
from common.misc import get_url

class StoreItem(models.Model):
    event = models.ForeignKey(
        Event,
        verbose_name=u'Tapahtuma',
        help_text=u'Tapahtuma johon tuote liittyy.',
        blank=True,
        null=True)
    name = models.CharField(
        u'Tuotteen nimi',
        help_text=u'Tuotteen lyhyt nimi.',
        max_length=255)
    description = models.TextField(
        u'Tuotteen kuvaus',
        help_text=u'Tuotteen pitkä kuvaus.')
    price = models.DecimalField(
        u'Tuotteen hinta',
        help_text=u'Tuotteen hinta.',
        max_digits=5,
        decimal_places=2)
    max = models.IntegerField(
        u'Kappaletta saatavilla',
        help_text=u'Kuinka monta kappaletta on ostettavissa ennen myynnin lopettamista.')
    available = models.BooleanField(
        u'Ostettavissa',
        default=False,
        help_text=u'Ilmoittaa, näkyykö tuote kaupassa.')
    imagefile_original = models.ImageField(
        u'Tuotekuva',
        upload_to='store/images/',
        help_text=u"Edustava kuva tuotteelle.", blank=True, null=True)
    imagefile_thumbnail = ImageSpecField(
        [ResizeToFill(64, 64)],
        source='imagefile_original',
        format='PNG')
    max_per_order = models.IntegerField(
        u'Maksimi per tilaus',
        default=10,
        help_text=u'Kuinka monta kappaletta voidaan ostaa kerralla.')
    sort_index = models.IntegerField(
        u'Järjestysarvo',
        default=0,
        help_text=u'Tuotteet esitetään kaupassa tämän luvun mukaan järjestettynä, pienempilukuiset ensin.')
    discount_amount = models.IntegerField(
        u'Alennusmäärä',
        default=-1,
        help_text=u'Pienin määrä tuotteita joka oikeuttaa alennukseen (-1 = ei mitään)')
    discount_percentage = models.IntegerField(
        u'Alennusprosentti',
        default=0,
        help_text=u'Alennuksen määrä prosentteina kun tuotteiden määrä saavuttaa alennusmäärän.')

    def is_discount_available(self):
        return self.discount_amount >= 0

    def get_discount_factor(self):
        return (100.0 - self.discount_percentage) / 100.0

    def get_discounted_unit_price(self, amount):
        """Returns decimal price of item considering any quantity discount."""
        if amount >= self.discount_amount and amount >= 0:
            factor = (Decimal(100) - Decimal(self.discount_percentage)) / 100
            price = self.price * factor
        else:
            price = self.price
        # If we need to consider rounding direction, this might be the place.
        # Pass "rounding=METHOD" as second argument.
        return price.quantize(Decimal('0.01'))

    def get_discounted_subtotal(self, amount):
        """Returns decimal subtotal for a specific number of items, considering
        any quantity discount."""
        return self.get_discounted_unit_price(amount) * amount

    def num_available(self):
        return min(self.max - self.num_sold(), self.max_per_order)

    def num_in_store(self):
        return self.max - self.num_sold()

    def num_sold(self):
        return TransactionItem.objects.filter(
            transaction__time_paid__isnull=False,
            item=self
        ).count()

    @staticmethod
    def items_available():
        return StoreItem.objects.filter(max__gt=0, available=True).order_by('sort_index')

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name = u"tuote"
        verbose_name_plural = u"tuotteet"

class StoreTransaction(models.Model):
    token = models.CharField(
        u'Palvelutunniste',
        help_text=u'Maksupalvelun maksukohtainen tunniste',
        max_length=255)
    time_created = models.DateTimeField(
        u'Luontiaika',
        null=True,
        blank=True)
    time_paid = models.DateTimeField(
        u'Maksun varmistumisaika',
        null=True,
        blank=True)
    time_pending = models.DateTimeField(
        u'Maksun maksuaika',
        null=True,
        blank=True)
    time_cancelled = models.DateTimeField(
        u'Peruutusaika',
        null=True,
        blank=True)
    payment_method_name = models.CharField(
        u'Maksutapa',
        help_text=u'Tapa jolla tilaus maksettiin',
        max_length=32,
        blank=True,
        default=u'')
    key = models.CharField(
        u'Avain',
        max_length=40,
        unique=True,
        help_text=u'Paikallinen maksukohtainen tunniste')
    firstname = models.CharField(
        u'Etunimi',
        max_length=64)
    lastname = models.CharField(
        u'Sukunimi',
        max_length=64)
    company = models.CharField\
        (u'Yritys',
         max_length=128,
         blank=True)
    email = models.EmailField(
        u'Sähköposti',
        max_length=255,
        help_text=u'Sähköpostiosoitteen on oltava toimiva, sillä liput ja tuotteiden lunastukseen '
                  u'tarvittavat koodit lähetetään sinne.')
    telephone = models.CharField(
        u'Puhelinnumero',
        max_length=64,
        blank=True)
    mobile = models.CharField(
        u'Matkapuhelin',
        max_length=64,
        blank=True)
    street = models.CharField(
        u'Katuosoite',
        max_length=128,
        help_text=u'Katusoite tarvitaan maksupalvelun vaatimuksesta.')
    postalcode = models.CharField(
        u'Postinumero',
        max_length=16)
    city = models.CharField(
        u'Postitoimipaikka',
        max_length=64)
    country = CountryField(
        u'Maa',
        default='FI')
    information = models.TextField(
        u'Lisätiedot',
        help_text=u'Mikäli tilaukseen kuuluu T-paitoja, määritä niiden koot tässä.',
        blank=True)

    @property
    def is_paid(self):
        return self.time_paid is not None

    @property
    def is_cancelled(self):
        return self.time_cancelled is not None

    @property
    def is_pending(self):
        return self.time_pending is not None

    @property
    def is_delivered(self):
        for item in self.get_transaction_items():
            if not item.is_delivered:
                return False
        return True

    @property
    def qr_code(self):
        return get_url(reverse('store:ta_view', kwargs={'transaction_key': self.key}))

    @property
    def full_name(self):
        return u'{} {}'.format(self.firstname, self.lastname)

    def get_status_text(self):
        if self.is_cancelled:
            return u'Peruutettu'
        if self.is_delivered:
            return u'Toimitettu'
        if self.is_paid:
            return u'Maksettu'
        if self.is_pending:
            return u'Vireillä'
        return u'Tuotteet valittu'

    def get_total_price(self):
        ret = 0
        for item in TransactionItem.objects.filter(transaction=self):
            ret += item.purchase_price
        return ret

    def get_transaction_items(self):
        return TransactionItem.objects.filter(transaction=self)

    def get_distinct_storeitems_and_prices(self):
        """Returns a list of unique (StoreItem, price) tuples related to
        this transaction."""
        # get (item-id, price) tuples
        items = self.get_transaction_items().values_list(
            'item', 'purchase_price').distinct()
        itemlist = []
        for key, price in items:
            # this isn't a performance issue with our database
            itemlist.append((StoreItem.objects.filter(pk=key).get(), price))
        return itemlist

    def get_storeitem_count(self, store_item):
        return TransactionItem.objects.filter(
            item=store_item, transaction=self).count()

    def __unicode__(self):
        return self.full_name

    class Meta:
        verbose_name = u"transaktio"
        verbose_name_plural = u"transaktiot"
        permissions = (("view_storetransaction", "Can view store transactions"),)

class TransactionItem(models.Model):
    key = models.CharField(
        u'Avain',
        max_length=40,
        unique=True,
        help_text=u'Lippuavain')
    item = models.ForeignKey(
        StoreItem,
        verbose_name=u'Tuote')
    transaction = models.ForeignKey(
        StoreTransaction,
        verbose_name=u'Ostotapahtuma')
    time_delivered = models.DateTimeField(
        u'Toimitusaika',
        null=True,
        blank=True)
    purchase_price = models.DecimalField(
        u'Tuotteen hinta',
        help_text=u'Tuotteen hinta ostoshetkellä',
        max_digits=5,
        decimal_places=2)
    original_price = models.DecimalField(
        u'Tuotteen alkuperäinen hinta',
        help_text=u'Tuotteen hinta ostoshetkellä ilman alennuksia',
        max_digits=5,
        decimal_places=2)

    @property
    def is_delivered(self):
        return self.time_delivered is not None

    @property
    def qr_code(self):
        return get_url(reverse('store:ti_view', kwargs={'item_key': self.key}))

    def __unicode__(self):
        return u'{} for {}'.format(self.item.name, self.transaction.full_name)

    class Meta:
        verbose_name = u"transaktiotuote"
        verbose_name_plural = u"transaktiotuotteet"
<|endoftext|>


<commit_before>"""
xbee.py

By Paul Malmsten, 2010
Inspired by code written by Amit Synderman and Marco Sangalli
pmalmsten@gmail.com

XBee superclass module

This class defines data and methods common to all XBee modules.
This class should be subclassed in order to provide
series-specific functionality.
"""
import struct, threading, time
from xbee.frame import APIFrame
from xbee.python2to3 import byteToInt, intToByte

class ThreadQuitException(Exception):
    pass

class CommandFrameException(KeyError):
    pass

class XBeeBase(threading.Thread):
    """
    Abstract base class providing command generation and response
    parsing methods for XBee modules.

    Constructor arguments:
        ser:    The file-like serial port to use.

        shorthand: boolean flag which determines whether shorthand command
                   calls (i.e. xbee.at(...) instead of xbee.send("at",...)
                   are allowed.

        callback: function which should be called with frame data
                  whenever a frame arrives from the serial port.
                  When this is not None, a background thread to monitor
                  the port and call the given function is automatically
                  started.

        escaped: boolean flag which determines whether the library should
                 operate in escaped mode. In this mode, certain data bytes
                 in the output and input streams will be escaped and unescaped
                 in accordance with the XBee API. This setting must match
                 the appropriate api_mode setting of an XBee device; see your
                 XBee device's documentation for more information.
    """

    def __init__(self, ser, shorthand=True, callback=None, escaped=False):
        super(XBeeBase, self).__init__()
        self.serial = ser
        self.shorthand = shorthand
        self._callback = None
        self._thread_continue = False
        self._escaped = escaped

        if callback:
            self._callback = callback
            self._thread_continue = True
            self.start()

    def halt(self):
        """
        halt: None -> None

        If this instance has a separate thread running, it will be
        halted. This method will wait until the thread has cleaned
        up before returning.
        """
        if self._callback:
            self._thread_continue = False
            self.join()

    def _write(self, data):
        """
        _write: binary data -> None

        Packages the given binary data in an API frame and writes the
        result to the serial port
        """
        frame = APIFrame(data, self._escaped).output()
        self.serial.write(frame)

    def run(self):
        """
        run: None -> None

        This method overrides threading.Thread.run() and is automatically
        called when an instance is created with threading enabled.
        """
        while True:
            try:
                self._callback(self.wait_read_frame())
            except ThreadQuitException:
                break

    def _wait_for_frame(self):
        """
        _wait_for_frame: None -> binary data

        _wait_for_frame will read from the serial port until a valid
        API frame arrives. It will then return the binary data
        contained within the frame.

        If this method is called as a separate thread
        and self.thread_continue is set to False, the thread will
        exit by raising a ThreadQuitException.
        """
        frame = APIFrame(escaped=self._escaped)

        while True:
            if self._callback and not self._thread_continue:
                raise ThreadQuitException

            if self.serial.inWaiting() == 0:
                time.sleep(.01)
                continue

            byte = self.serial.read()

            if byte != APIFrame.START_BYTE:
                continue

            # Save all following bytes, if they are not empty
            if len(byte) == 1:
                frame.fill(byte)

            while(frame.remaining_bytes() > 0):
                byte = self.serial.read()

                if len(byte) == 1:
                    frame.fill(byte)

            try:
                # Try to parse and return result
                frame.parse()
                return frame
            except ValueError:
                # Bad frame, so restart
                frame = APIFrame(escaped=self._escaped)

    def _build_command(self, cmd, **kwargs):
        """
        _build_command: string (binary data) ... -> binary data

        _build_command will construct a command packet according to the
        specified command's specification in api_commands. It will expect
        named arguments for all fields other than those with a default
        value or a length of 'None'.

        Each field will be written out in the order they are defined
        in the command definition.
        """
        try:
            cmd_spec = self.api_commands[cmd]
        except AttributeError:
            raise NotImplementedError("API command specifications could not be found; use a derived class which defines 'api_commands'.")

        packet = b''

        for field in cmd_spec:
            try:
                # Read this field's name from the function arguments dict
                data = kwargs[field['name']]
            except KeyError:
                # Data wasn't given
                # Only a problem if the field has a specific length
                if field['len'] is not None:
                    # Was a default value specified?
                    default_value = field['default']
                    if default_value:
                        # If so, use it
                        data = default_value
                    else:
                        # Otherwise, fail
                        raise KeyError(
                            "The expected field %s of length %d was not provided"
                            % (field['name'], field['len']))
                else:
                    # No specific length, ignore it
                    data = None

            # Ensure that the proper number of elements will be written
            if field['len'] and len(data) != field['len']:
                raise ValueError(
                    "The data provided for '%s' was not %d bytes long"\
                    % (field['name'], field['len']))

            # Add the data to the packet, if it has been specified
            # Otherwise, the parameter was of variable length, and not
            #  given
            if data:
                packet += data

        return packet

    def _split_response(self, data):
        """
        _split_response: binary data -> {'id':str,
                                         'param':binary data,
                                         ...}

        _split_response takes a data packet received from an XBee device
        and converts it into a dictionary. This dictionary provides
        names for each segment of binary data as specified in the
        api_responses spec.
        """
        # Fetch the first byte, identify the packet
        # If the spec doesn't exist, raise exception
        packet_id = data[0:1]
        try:
            packet = self.api_responses[packet_id]
        except AttributeError:
            raise NotImplementedError("API response specifications could not be found; use a derived class which defines 'api_responses'.")
        except KeyError:
            # Check to see if this ID can be found among transmittible packets
            for cmd_name, cmd in list(self.api_commands.items()):
                if cmd[0]['default'] == data[0:1]:
                    raise CommandFrameException("Incoming frame with id %s looks like a command frame of type '%s' (these should not be received). Are you sure your devices are in API mode?"
                            % (data[0], cmd_name))

            raise KeyError(
                "Unrecognized response packet with id byte {0}".format(data[0]))

        # Current byte index in the data stream
        index = 1

        # Result info
        info = {'id':packet['name']}
        packet_spec = packet['structure']

        # Parse the packet in the order specified
        for field in packet_spec:
            if field['len'] == 'null_terminated':
                field_data = b''

                while data[index:index+1] != b'\x00':
                    field_data += data[index:index+1]
                    index += 1

                index += 1
                info[field['name']] = field_data
            elif field['len'] is not None:
                # Store the number of bytes specified

                # Are we trying to read beyond the last data element?
                if index + field['len'] > len(data):
                    raise ValueError(
                        "Response packet was shorter than expected")

                field_data = data[index:index + field['len']]
                info[field['name']] = field_data

                index += field['len']
            # If the data field has no length specified, store any
            #  leftover bytes and quit
            else:
                field_data = data[index:]

                # Were there any remaining bytes?
                if field_data:
                    # If so, store them
                    info[field['name']] = field_data
                    index += len(field_data)
                break

        # If there are more bytes than expected, raise an exception
        if index < len(data):
            raise ValueError(
                "Response packet was longer than expected; expected: %d, got: %d bytes" % (index,
                                                                                len(data)))

        # Apply parsing rules if any exist
        if 'parsing' in packet:
            for parse_rule in packet['parsing']:
                # Only apply a rule if it is relevant (raw data is available)
                if parse_rule[0] in info:
                    # Apply the parse function to the indicated field and
                    # replace the raw data with the result
                    info[parse_rule[0]] = parse_rule[1](self, info)

        return info

    def _parse_samples_header(self, io_bytes):
        """
        _parse_samples_header: binary data in XBee IO data format ->
                        (int, [int ...], [int ...], int, int)

        _parse_samples_header will read the first three bytes of the
        binary data given and will return the number of samples which
        follow, a list of enabled digital inputs, a list of enabled
        analog inputs, the dio_mask, and the size of the header in bytes
        """
        header_size = 3

        # number of samples (always 1?) is the first byte
        sample_count = byteToInt(io_bytes[0])

        # part of byte 1 and byte 2 are the DIO mask ( 9 bits )
        dio_mask = (byteToInt(io_bytes[1]) << 8 | byteToInt(io_bytes[2])) & 0x01FF

        # upper 7 bits of byte 1 is the AIO mask
        aio_mask = (byteToInt(io_bytes[1]) & 0xFE) >> 1

        # sorted lists of enabled channels; value is position of bit in mask
        dio_chans = []
        aio_chans = []

        for i in range(0,9):
            if dio_mask & (1 << i):
                dio_chans.append(i)

        dio_chans.sort()

        for i in range(0,7):
            if aio_mask & (1 << i):
                aio_chans.append(i)

        aio_chans.sort()

        return (sample_count, dio_chans, aio_chans, dio_mask, header_size)

    def _parse_samples(self, io_bytes):
        """
        _parse_samples: binary data in XBee IO data format ->
                        [ {"dio-0":True,
                           "dio-1":False,
                           "adc-0":100"}, ...]

        _parse_samples reads binary data from an XBee device in the IO
        data format specified by the API. It will then return a
        dictionary indicating the status of each enabled IO port.
        """

        sample_count, dio_chans, aio_chans, dio_mask, header_size = \
            self._parse_samples_header(io_bytes)

        samples = []

        # split the sample data into a list, so it can be pop()'d
        sample_bytes = [byteToInt(c) for c in io_bytes[header_size:]]

        # repeat for every sample provided
        for sample_ind in range(0, sample_count):
            tmp_samples = {}

            if dio_chans:
                # we have digital data
                digital_data_set = (sample_bytes.pop(0) << 8 | sample_bytes.pop(0))
                digital_values = dio_mask & digital_data_set

                for i in dio_chans:
                    tmp_samples['dio-{0}'.format(i)] = True if (digital_values >> i) & 1 else False

            for i in aio_chans:
                # only first 10 bits are significant
                analog_sample = (sample_bytes.pop(0) << 8 | sample_bytes.pop(0)) & 0x03FF
                tmp_samples['adc-{0}'.format(i)] = analog_sample

            samples.append(tmp_samples)

        return samples

    def send(self, cmd, **kwargs):
        """
        send: string param=binary data ... -> None

        When send is called with the proper arguments, an API command
        will be written to the serial port for this XBee device
        containing the proper instructions and data.

        This method must be called with named arguments in accordance
        with the api_command specification. Arguments matching all
        field names other than those in reserved_names (like 'id' and
        'order') should be given, unless they are of variable length
        (of 'None' in the specification. Those are optional).
        """
        # Pass through the keyword arguments
        self._write(self._build_command(cmd, **kwargs))

    def wait_read_frame(self):
        """
        wait_read_frame: None -> frame info dictionary

        wait_read_frame calls XBee._wait_for_frame() and waits until a
        valid frame appears on the serial port. Once it receives a frame,
        wait_read_frame attempts to parse the data contained within it
        and returns the resulting dictionary
        """

        frame = self._wait_for_frame()
        return self._split_response(frame.data)

    def __getattr__(self, name):
        """
        If a method by the name of a valid api command is called,
        the arguments will be automatically sent to an appropriate
        send() call
        """

        # If api_commands is not defined, raise NotImplementedError\
        #  If its not defined, _getattr__ will be called with its name
        if name == 'api_commands':
            raise NotImplementedError("API command specifications could not be found; use a derived class which defines 'api_commands'.")

        # Is shorthand enabled, and is the called name a command?
        if self.shorthand and name in self.api_commands:
            # If so, simply return a function which passes its arguments
            # to an appropriate send() call
            return lambda **kwargs: self.send(name, **kwargs)
        else:
            raise AttributeError("XBee has no attribute '%s'" % name)
<commit_msg>changes to xbee threading/ serial handling<commit_after>"""
xbee.py

By Paul Malmsten, 2010
Inspired by code written by Amit Synderman and Marco Sangalli
pmalmsten@gmail.com

XBee superclass module

This class defines data and methods common to all XBee modules.
This class should be subclassed in order to provide
series-specific functionality.
"""
import struct, threading, time
from xbee.frame import APIFrame
from xbee.python2to3 import byteToInt, intToByte

class ThreadQuitException(Exception):
    pass

class CommandFrameException(KeyError):
    pass

class XBeeBase():
    """
    Abstract base class providing command generation and response
    parsing methods for XBee modules.

    Constructor arguments:
        ser:    The file-like serial port to use.

        shorthand: boolean flag which determines whether shorthand command
                   calls (i.e. xbee.at(...) instead of xbee.send("at",...)
                   are allowed.

        callback: function which should be called with frame data
                  whenever a frame arrives from the serial port.
                  When this is not None, a background thread to monitor
                  the port and call the given function is automatically
                  started.

        escaped: boolean flag which determines whether the library should
                 operate in escaped mode. In this mode, certain data bytes
                 in the output and input streams will be escaped and unescaped
                 in accordance with the XBee API. This setting must match
                 the appropriate api_mode setting of an XBee device; see your
                 XBee device's documentation for more information.
    """

    def __init__(self, ser=None, shorthand=True, callback=None, escaped=False, start_callback=None):
        super(XBeeBase, self).__init__()

        if isinstance(ser, serial.Serial):
            self.serial = ser
            self.serial_opts = None
        else:
            self.serial = None
            self.serial_opts = ser

        self.shorthand = shorthand
        self._callback = callback
        self._start_callback = start_callback
        self._exit = threading.Event()
        self._escaped = escaped
        self._thread = None

#        if callback:
#            self.start()

    def start(self):
        if not self._callback: return
        self._exit.clear()
        self._thread = threading.Thread(
                target=self.run,
                name="XBee @ %s" % ser.port )
        self._thread.daemon = True
        self._thread.start()

    def halt(self):
        """
        halt: None -> None

        If this instance has a separate thread running, it will be
        halted. This method will wait until the thread has cleaned
        up before returning.
        """
        if not self._callback: return
        self._exit.set()
        self._thread.join(10)

    def _write(self, data):
        """
        _write: binary data -> None

        Packages the given binary data in an API frame and writes the
        result to the serial port
        """
        frame = APIFrame(data, self._escaped).output()
        self.serial.write(frame)

    def run(self):
        """
        run: None -> None

        This method overrides threading.Thread.run() and is automatically
        called when an instance is created with threading enabled.
        """

        if self.serial is not None and self.start_callback:
            self.start_callback(self)

        while not self.exit.is_set():
            try:
                if self.serial is None:
                    # allows the code to recover/ initialize if the USB device
                    # is unplugged after this code is running:
                    if not os.path.exists( self.serial_opts['port'] ):
                        self._exit.wait(10) # wait before retry
                        continue
                    # self.serial may be a dict of init params
                    self.serial = serial.Serial(**self.serial_opts)
                    if self.start_callback: self.start_callback(self)

                self._callback(self.wait_read_frame())

            except serial.SerialException as ex:
                self._exit.wait(2)
                try: # try to re-init the device
                    if self.serial is not None: self.serial.open()
                except Exception, msg:
                    # worst case, completely re-init the device
                    print "Unexpected error re-opening serial port! %s" % msg
                    self.serial = None
            except Exception, msg:
                print "XBee: Unexpected error! %s" % msg
            except ThreadQuitException:
                break

        self.serial.close()

    def _wait_for_frame(self):
        """
        _wait_for_frame: None -> binary data

        _wait_for_frame will read from the serial port until a valid
        API frame arrives. It will then return the binary data
        contained within the frame.

        If this method is called as a separate thread
        and self.thread_continue is set to False, the thread will
        exit by raising a ThreadQuitException.
        """
        frame = APIFrame(escaped=self._escaped)

        while True:
            if self._exit.is_set(): raise ThreadQuitException

#            if self.serial.inWaiting() == 0:
#                self._exit.wait(.01)
#                continue

            byte = self.serial.read()

            if byte != APIFrame.START_BYTE:
                continue

            # Save all following bytes, if they are not empty
            if len(byte) == 1:
                frame.fill(byte)

            while(frame.remaining_bytes() > 0):
                byte = self.serial.read()

                if len(byte) == 1:
                    frame.fill(byte)

            try:
                # Try to parse and return result
                frame.parse()
                return frame
            except ValueError:
                # Bad frame, so restart
                frame = APIFrame(escaped=self._escaped)

    def _build_command(self, cmd, **kwargs):
        """
        _build_command: string (binary data) ... -> binary data

        _build_command will construct a command packet according to the
        specified command's specification in api_commands. It will expect
        named arguments for all fields other than those with a default
        value or a length of 'None'.

        Each field will be written out in the order they are defined
        in the command definition.
        """
        try:
            cmd_spec = self.api_commands[cmd]
        except AttributeError:
            raise NotImplementedError("API command specifications could not be found; use a derived class which defines 'api_commands'.")

        packet = b''

        for field in cmd_spec:
            try:
                # Read this field's name from the function arguments dict
                data = kwargs[field['name']]
            except KeyError:
                # Data wasn't given
                # Only a problem if the field has a specific length
                if field['len'] is not None:
                    # Was a default value specified?
                    default_value = field['default']
                    if default_value:
                        # If so, use it
                        data = default_value
                    else:
                        # Otherwise, fail
                        raise KeyError(
                            "The expected field %s of length %d was not provided"
                            % (field['name'], field['len']))
                else:
                    # No specific length, ignore it
                    data = None

            # Ensure that the proper number of elements will be written
            if field['len'] and len(data) != field['len']:
                raise ValueError(
                    "The data provided for '%s' was not %d bytes long"\
                    % (field['name'], field['len']))

            # Add the data to the packet, if it has been specified
            # Otherwise, the parameter was of variable length, and not
            #  given
            if data:
                packet += data

        return packet

    def _split_response(self, data):
        """
        _split_response: binary data -> {'id':str,
                                         'param':binary data,
                                         ...}

        _split_response takes a data packet received from an XBee device
        and converts it into a dictionary. This dictionary provides
        names for each segment of binary data as specified in the
        api_responses spec.
        """
        # Fetch the first byte, identify the packet
        # If the spec doesn't exist, raise exception
        packet_id = data[0:1]
        try:
            packet = self.api_responses[packet_id]
        except AttributeError:
            raise NotImplementedError("API response specifications could not be found; use a derived class which defines 'api_responses'.")
        except KeyError:
            # Check to see if this ID can be found among transmittible packets
            for cmd_name, cmd in list(self.api_commands.items()):
                if cmd[0]['default'] == data[0:1]:
                    raise CommandFrameException("Incoming frame with id %s looks like a command frame of type '%s' (these should not be received). Are you sure your devices are in API mode?"
                            % (data[0], cmd_name))

            raise KeyError(
                "Unrecognized response packet with id byte {0}".format(data[0]))

        # Current byte index in the data stream
        index = 1

        # Result info
        info = {'id':packet['name']}
        packet_spec = packet['structure']

        # Parse the packet in the order specified
        for field in packet_spec:
            if field['len'] == 'null_terminated':
                field_data = b''

                while data[index:index+1] != b'\x00':
                    field_data += data[index:index+1]
                    index += 1

                index += 1
                info[field['name']] = field_data
            elif field['len'] is not None:
                # Store the number of bytes specified

                # Are we trying to read beyond the last data element?
                if index + field['len'] > len(data):
                    raise ValueError(
                        "Response packet was shorter than expected")

                field_data = data[index:index + field['len']]
                info[field['name']] = field_data

                index += field['len']
            # If the data field has no length specified, store any
            #  leftover bytes and quit
            else:
                field_data = data[index:]

                # Were there any remaining bytes?
                if field_data:
                    # If so, store them
                    info[field['name']] = field_data
                    index += len(field_data)
                break

        # If there are more bytes than expected, raise an exception
        if index < len(data):
            raise ValueError(
                "Response packet was longer than expected; expected: %d, got: %d bytes" % (index,
                                                                                len(data)))

        # Apply parsing rules if any exist
        if 'parsing' in packet:
            for parse_rule in packet['parsing']:
                # Only apply a rule if it is relevant (raw data is available)
                if parse_rule[0] in info:
                    # Apply the parse function to the indicated field and
                    # replace the raw data with the result
                    info[parse_rule[0]] = parse_rule[1](self, info)

        return info

    def _parse_samples_header(self, io_bytes):
        """
        _parse_samples_header: binary data in XBee IO data format ->
                        (int, [int ...], [int ...], int, int)

        _parse_samples_header will read the first three bytes of the
        binary data given and will return the number of samples which
        follow, a list of enabled digital inputs, a list of enabled
        analog inputs, the dio_mask, and the size of the header in bytes
        """
        header_size = 3

        # number of samples (always 1?) is the first byte
        sample_count = byteToInt(io_bytes[0])

        # part of byte 1 and byte 2 are the DIO mask ( 9 bits )
        dio_mask = (byteToInt(io_bytes[1]) << 8 | byteToInt(io_bytes[2])) & 0x01FF

        # upper 7 bits of byte 1 is the AIO mask
        aio_mask = (byteToInt(io_bytes[1]) & 0xFE) >> 1

        # sorted lists of enabled channels; value is position of bit in mask
        dio_chans = []
        aio_chans = []

        for i in range(0,9):
            if dio_mask & (1 << i):
                dio_chans.append(i)

        dio_chans.sort()

        for i in range(0,7):
            if aio_mask & (1 << i):
                aio_chans.append(i)

        aio_chans.sort()

        return (sample_count, dio_chans, aio_chans, dio_mask, header_size)

    def _parse_samples(self, io_bytes):
        """
        _parse_samples: binary data in XBee IO data format ->
                        [ {"dio-0":True,
                           "dio-1":False,
                           "adc-0":100"}, ...]

        _parse_samples reads binary data from an XBee device in the IO
        data format specified by the API. It will then return a
        dictionary indicating the status of each enabled IO port.
        """

        sample_count, dio_chans, aio_chans, dio_mask, header_size = \
            self._parse_samples_header(io_bytes)

        samples = []

        # split the sample data into a list, so it can be pop()'d
        sample_bytes = [byteToInt(c) for c in io_bytes[header_size:]]

        # repeat for every sample provided
        for sample_ind in range(0, sample_count):
            tmp_samples = {}

            if dio_chans:
                # we have digital data
                digital_data_set = (sample_bytes.pop(0) << 8 | sample_bytes.pop(0))
                digital_values = dio_mask & digital_data_set

                for i in dio_chans:
                    tmp_samples['dio-{0}'.format(i)] = True if (digital_values >> i) & 1 else False

            for i in aio_chans:
                # only first 10 bits are significant
                analog_sample = (sample_bytes.pop(0) << 8 | sample_bytes.pop(0)) & 0x03FF
                tmp_samples['adc-{0}'.format(i)] = analog_sample

            samples.append(tmp_samples)

        return samples

    def send(self, cmd, **kwargs):
        """
        send: string param=binary data ... -> None

        When send is called with the proper arguments, an API command
        will be written to the serial port for this XBee device
        containing the proper instructions and data.

        This method must be called with named arguments in accordance
        with the api_command specification. Arguments matching all
        field names other than those in reserved_names (like 'id' and
        'order') should be given, unless they are of variable length
        (of 'None' in the specification. Those are optional).
        """
        # Pass through the keyword arguments
        self._write(self._build_command(cmd, **kwargs))

    def wait_read_frame(self):
        """
        wait_read_frame: None -> frame info dictionary

        wait_read_frame calls XBee._wait_for_frame() and waits until a
        valid frame appears on the serial port. Once it receives a frame,
        wait_read_frame attempts to parse the data contained within it
        and returns the resulting dictionary
        """

        frame = self._wait_for_frame()
        return self._split_response(frame.data)

    def __getattr__(self, name):
        """
        If a method by the name of a valid api command is called,
        the arguments will be automatically sent to an appropriate
        send() call
        """

        # If api_commands is not defined, raise NotImplementedError\
        #  If its not defined, _getattr__ will be called with its name
        if name == 'api_commands':
            raise NotImplementedError("API command specifications could not be found; use a derived class which defines 'api_commands'.")

        # Is shorthand enabled, and is the called name a command?
        if self.shorthand and name in self.api_commands:
            # If so, simply return a function which passes its arguments
            # to an appropriate send() call
            return lambda **kwargs: self.send(name, **kwargs)
        else:
            raise AttributeError("XBee has no attribute '%s'" % name)
<|endoftext|>


<commit_before>from __future__ import absolute_import

from copy import deepcopy
import re

import ldap
try:
    from passlib.hash import ldap_md5_crypt
except ImportError:
    pass

from .recording import SeedRequired, RecordableMethods, recorded

class LDAPObject(RecordableMethods):
    """
    :param directory: The initial content of this LDAP connection.
    :type directory: ``{dn: {attr: [values]}}``

    Our mock replacement for :class:`ldap.LDAPObject`. This exports selected
    LDAP operations and allows you to set return values in advance as well as
    discover which methods were called after the fact.

    All of these methods take the same arguments as their python-ldap
    counterparts. Some are self-explanatory; those that are only partially
    implemented are documented as such.

    .. attribute:: options

        *dict*: Options that have been set by
        :meth:`~mockldap.LDAPObject.set_option`.

    .. attribute:: tls_enabled

        *bool*: True if :meth:`~mockldap.LDAPObject.start_tls_s` was called.

    .. attribute:: bound_as

        *string*: DN of the last successful bind. None if unbound.
    """
    def __init__(self, directory):
        self.directory = ldap.cidict.cidict(deepcopy(directory))
        self.async_results = []
        self.options = {}
        self.tls_enabled = False
        self.bound_as = None

    #
    # Begin LDAP methods
    #

    @recorded
    def initialize(self, *args, **kwargs):
        """ This only exists for recording purposes. """
        pass

    @recorded
    def get_option(self, option):
        """
        """
        return self.options[option]

    @recorded
    def set_option(self, option, invalue):
        """
        """
        self.options[option] = invalue

    @recorded
    def simple_bind_s(self, who='', cred=''):
        """
        """
        success = False

        if(who == '' and cred == ''):
            success = True
        elif self._compare_s(who, 'userPassword', cred):
            success = True

        if success:
            self.bound_as = who
            return (97, []) # python-ldap returns this; I don't know what it means
        else:
            raise ldap.INVALID_CREDENTIALS('%s:%s' % (who, cred))

    @recorded
    def search(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
        """
        Implements searching with simple filters of the form (attr=value),
        where value can be a string or *. attrlist and attrsonly are also
        supported. Beyond that, this method must be seeded.
        """
        value = self._search_s(base, scope, filterstr, attrlist, attrsonly)

        return self._add_async_result(value)

    @recorded
    def result(self, msgid, all=1, timeout=None):
        """
        """
        return ldap.RES_SEARCH_RESULT, self._pop_async_result(msgid)

    @recorded
    def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
        """
        Implements searching with simple filters of the form (attr=value),
        where value can be a string or *. attrlist and attrsonly are also
        supported. Beyond that, this method must be seeded.
        """
        return self._search_s(base, scope, filterstr, attrlist, attrsonly)

    @recorded
    def start_tls_s(self):
        """
        """
        self.tls_enabled = True

    @recorded
    def compare_s(self, dn, attr, value):
        """
        """
        return self._compare_s(dn, attr, value)

    @recorded
    def add_s(self, dn, record):
        """
        """
        return self._add_s(dn, record)

    @recorded
    def unbind(self):
        """
        """
        self.bound_as = None

    @recorded
    def unbind_s(self):
        """
        """
        self.bound_as = None

    @recorded
    def modify_s(self, dn, record):
        """
        """
        return self._modify_s(dn, record)

    @recorded
    def modify_ext_s(self, dn, record, serverctrls=None, clientctrls=None):
        """
        """
        return self._modify_ext_s(dn, record, serverctrls, clientctrls)

    @recorded
    def delete_s(self, dn):
        """
        """
        return self._delete_s(dn)

    #
    # Internal implementations
    #

    def _value_as(self, value):
        if not isinstance(value, list):
            return [value]
        return value

    def _dn(self, dn):
        return str(dn).lower()

    def _delete_s(self, dn):
        dn = self._dn(dn)
        try:
            del self.directory[dn]
        except KeyError:
            return (105, [], len(self.methods_called()), [])

    def _modify_s(self, dn, record):
        self._modify_ext_s(dn=dn, record=record, serverctrls=None, clientctrls=None)

    def _modify_ext_s(self, dn, record, serverctrls, clientctrls):
        dn = self._dn(dn)
        for item in record:
            val = [] if item[2] is None else self._value_as(item[2])
            self.directory.setdefault(dn, {})
            self.directory[dn].setdefault(item[1], [])
            if item[0] == ldap.MOD_ADD:
                self.directory[dn][item[1]] += val
            elif item[0] == ldap.MOD_DELETE:
                index = self.directory[dn][item[1]].index(val)
                if val is None:
                    self.directory[dn][item[1]] = []
                else:
                    del self.directory[dn][item[1]][index]
            elif item[0] == ldap.MOD_REPLACE:
                try:
                    index = self.directory[dn][item[1]].index(val)
                    self.directory[dn][item[1]][index] = val
                except ValueError:
                    self.directory[dn][item[1]] = val
            else: raise Exception("Unknown LDAP operation")

    def _compare_s(self, dn, attr, value):
        dn = self._dn(dn)
        if dn not in self.directory:
            raise ldap.NO_SUCH_OBJECT

        if attr not in self.directory[dn]:
            raise ldap.NO_SUCH_ATTRIBUTE

        if attr == 'userPassword':
            for password in self.directory[dn][attr]:
                try:
                    # TODO: Implement more ldap pwd hashes from passlib
                    # http://pythonhosted.org/passlib/lib/passlib.hash.html#ldap-hashes
                    if ldap_md5_crypt.verify(value, password):
                        return 1
                except (NameError, ValueError):
                    pass
        return (value in self.directory[dn][attr]) and 1 or 0

    def _search_s(self, base, scope, filterstr, attrlist, attrsonly):
        valid_filterstr = re.compile(r'\(\w+=([\w@.]+|[*])\)')

        if not valid_filterstr.match(filterstr):
            raise SeedRequired('search_s("%s", %d, "%s", "%s", %d)' %
                (base, scope, filterstr, attrlist, attrsonly))

        def check_dn(dn, all_dn):
            if dn not in all_dn:
                raise ldap.NO_SUCH_OBJECT

        def get_results(dn, filterstr, results):
            attrs = self.directory.get(dn)
            attr, value = filterstr[1:-1].split('=')
            if attrs and attr in attrs.keys() and str(value) in attrs[attr] or value == u'*':
                new_attrs = attrs.copy()
                if attrlist or attrsonly:
                    for item in new_attrs.keys():
                        if attrsonly:
                            new_attrs[item] = []
                        if attrlist and item not in attrlist:
                            del(new_attrs[item])
                results.append((dn, new_attrs))

        results = []
        all_dn = self.directory.keys()
        if scope == ldap.SCOPE_BASE:
            check_dn(base, all_dn)
            get_results(base, filterstr, results)
        elif scope == ldap.SCOPE_ONELEVEL:
            for dn in all_dn:
                check_dn(dn, all_dn)
                if len(dn.split('=')) == len(base.split('=')) + 1 and dn.endswith(base):
                    get_results(dn, filterstr, results)
        elif scope == ldap.SCOPE_SUBTREE:
            for dn in all_dn:
                check_dn(dn, all_dn)
                if dn.endswith(base):
                    get_results(dn, filterstr, results)

        return results

    def _add_s(self, dn, record):
        dn = self._dn(dn)
        entry = {}
        for item in record:
            entry[item[0]] = self._value_as(item[1])
        try:
            self.directory[dn]
            raise ldap.ALREADY_EXISTS
        except KeyError:
            self.directory[dn] = entry
            return (105, [], len(self.methods_called()), [])

    #
    # Async
    #

    def _add_async_result(self, value):
        self.async_results.append(value)

        return len(self.async_results) - 1

    def _pop_async_result(self, msgid):
        if msgid in xrange(len(self.async_results)):
            value = self.async_results[msgid]
            self.async_results[msgid] = None
        else:
            value = None

        return value
<commit_msg>Fix for MOD_DELETE<commit_after>from __future__ import absolute_import

from copy import deepcopy
import re

import ldap
try:
    from passlib.hash import ldap_md5_crypt
except ImportError:
    pass

from .recording import SeedRequired, RecordableMethods, recorded

class LDAPObject(RecordableMethods):
    """
    :param directory: The initial content of this LDAP connection.
    :type directory: ``{dn: {attr: [values]}}``

    Our mock replacement for :class:`ldap.LDAPObject`. This exports selected
    LDAP operations and allows you to set return values in advance as well as
    discover which methods were called after the fact.

    All of these methods take the same arguments as their python-ldap
    counterparts. Some are self-explanatory; those that are only partially
    implemented are documented as such.

    .. attribute:: options

        *dict*: Options that have been set by
        :meth:`~mockldap.LDAPObject.set_option`.

    .. attribute:: tls_enabled

        *bool*: True if :meth:`~mockldap.LDAPObject.start_tls_s` was called.

    .. attribute:: bound_as

        *string*: DN of the last successful bind. None if unbound.
    """
    def __init__(self, directory):
        self.directory = ldap.cidict.cidict(deepcopy(directory))
        self.async_results = []
        self.options = {}
        self.tls_enabled = False
        self.bound_as = None

    #
    # Begin LDAP methods
    #

    @recorded
    def initialize(self, *args, **kwargs):
        """ This only exists for recording purposes. """
        pass

    @recorded
    def get_option(self, option):
        """
        """
        return self.options[option]

    @recorded
    def set_option(self, option, invalue):
        """
        """
        self.options[option] = invalue

    @recorded
    def simple_bind_s(self, who='', cred=''):
        """
        """
        success = False

        if(who == '' and cred == ''):
            success = True
        elif self._compare_s(who, 'userPassword', cred):
            success = True

        if success:
            self.bound_as = who
            return (97, []) # python-ldap returns this; I don't know what it means
        else:
            raise ldap.INVALID_CREDENTIALS('%s:%s' % (who, cred))

    @recorded
    def search(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
        """
        Implements searching with simple filters of the form (attr=value),
        where value can be a string or *. attrlist and attrsonly are also
        supported. Beyond that, this method must be seeded.
        """
        value = self._search_s(base, scope, filterstr, attrlist, attrsonly)

        return self._add_async_result(value)

    @recorded
    def result(self, msgid, all=1, timeout=None):
        """
        """
        return ldap.RES_SEARCH_RESULT, self._pop_async_result(msgid)

    @recorded
    def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0):
        """
        Implements searching with simple filters of the form (attr=value),
        where value can be a string or *. attrlist and attrsonly are also
        supported. Beyond that, this method must be seeded.
        """
        return self._search_s(base, scope, filterstr, attrlist, attrsonly)

    @recorded
    def start_tls_s(self):
        """
        """
        self.tls_enabled = True

    @recorded
    def compare_s(self, dn, attr, value):
        """
        """
        return self._compare_s(dn, attr, value)

    @recorded
    def add_s(self, dn, record):
        """
        """
        return self._add_s(dn, record)

    @recorded
    def unbind(self):
        """
        """
        self.bound_as = None

    @recorded
    def unbind_s(self):
        """
        """
        self.bound_as = None

    @recorded
    def modify_s(self, dn, record):
        """
        """
        return self._modify_s(dn, record)

    @recorded
    def modify_ext_s(self, dn, record, serverctrls=None, clientctrls=None):
        """
        """
        return self._modify_ext_s(dn, record, serverctrls, clientctrls)

    @recorded
    def delete_s(self, dn):
        """
        """
        return self._delete_s(dn)

    #
    # Internal implementations
    #

    def _value_as(self, value):
        if not isinstance(value, list):
            return [value]
        return value

    def _dn(self, dn):
        return str(dn).lower()

    def _delete_s(self, dn):
        dn = self._dn(dn)
        try:
            del self.directory[dn]
        except KeyError:
            return (105, [], len(self.methods_called()), [])

    def _modify_s(self, dn, record):
        self._modify_ext_s(dn=dn, record=record, serverctrls=None, clientctrls=None)

    def _modify_ext_s(self, dn, record, serverctrls, clientctrls):
        dn = self._dn(dn)
        for item in record:
            val = [] if item[2] is None else self._value_as(item[2])
            self.directory.setdefault(dn, {})
            self.directory[dn].setdefault(item[1], [])
            if item[0] == ldap.MOD_ADD:
                self.directory[dn][item[1]] += val
            elif item[0] == ldap.MOD_DELETE:
                for record in val:
                    index = self.directory[dn][item[1]].index(record)
                    if record is None:
                        self.directory[dn][item[1]] = []
                    else:
                        del self.directory[dn][item[1]][index]
            elif item[0] == ldap.MOD_REPLACE:
                try:
                    index = self.directory[dn][item[1]].index(val)
                    self.directory[dn][item[1]][index] = val
                except ValueError:
                    self.directory[dn][item[1]] = val
            else: raise Exception("Unknown LDAP operation")

    def _compare_s(self, dn, attr, value):
        dn = self._dn(dn)
        if dn not in self.directory:
            raise ldap.NO_SUCH_OBJECT

        if attr not in self.directory[dn]:
            raise ldap.NO_SUCH_ATTRIBUTE

        if attr == 'userPassword':
            for password in self.directory[dn][attr]:
                try:
                    # TODO: Implement more ldap pwd hashes from passlib
                    # http://pythonhosted.org/passlib/lib/passlib.hash.html#ldap-hashes
                    if ldap_md5_crypt.verify(value, password):
                        return 1
                except (NameError, ValueError):
                    pass
        return (value in self.directory[dn][attr]) and 1 or 0

    def _search_s(self, base, scope, filterstr, attrlist, attrsonly):
        valid_filterstr = re.compile(r'\(\w+=([\w@.]+|[*])\)')

        if not valid_filterstr.match(filterstr):
            raise SeedRequired('search_s("%s", %d, "%s", "%s", %d)' %
                (base, scope, filterstr, attrlist, attrsonly))

        def check_dn(dn, all_dn):
            if dn not in all_dn:
                raise ldap.NO_SUCH_OBJECT

        def get_results(dn, filterstr, results):
            attrs = self.directory.get(dn)
            attr, value = filterstr[1:-1].split('=')
            if attrs and attr in attrs.keys() and str(value) in attrs[attr] or value == u'*':
                new_attrs = attrs.copy()
                if attrlist or attrsonly:
                    for item in new_attrs.keys():
                        if attrsonly:
                            new_attrs[item] = []
                        if attrlist and item not in attrlist:
                            del(new_attrs[item])
                results.append((dn, new_attrs))

        results = []
        all_dn = self.directory.keys()
        if scope == ldap.SCOPE_BASE:
            check_dn(base, all_dn)
            get_results(base, filterstr, results)
        elif scope == ldap.SCOPE_ONELEVEL:
            for dn in all_dn:
                check_dn(dn, all_dn)
                if len(dn.split('=')) == len(base.split('=')) + 1 and dn.endswith(base):
                    get_results(dn, filterstr, results)
        elif scope == ldap.SCOPE_SUBTREE:
            for dn in all_dn:
                check_dn(dn, all_dn)
                if dn.endswith(base):
                    get_results(dn, filterstr, results)

        return results

    def _add_s(self, dn, record):
        dn = self._dn(dn)
        entry = {}
        for item in record:
            entry[item[0]] = self._value_as(item[1])
        try:
            self.directory[dn]
            raise ldap.ALREADY_EXISTS
        except KeyError:
            self.directory[dn] = entry
            return (105, [], len(self.methods_called()), [])

    #
    # Async
    #

    def _add_async_result(self, value):
        self.async_results.append(value)

        return len(self.async_results) - 1

    def _pop_async_result(self, msgid):
        if msgid in xrange(len(self.async_results)):
            value = self.async_results[msgid]
            self.async_results[msgid] = None
        else:
            value = None

        return value
<|endoftext|>


<commit_before># coding: utf-8
# Copyright 2013 The Font Bakery Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# See AUTHORS.txt for the list of Authors and LICENSE.txt for the License.

from datetime import datetime
import os
import yaml
from flask import current_app, json
from ..decorators import lazy_property
from ..extensions import db
from ..tasks import process_project, prun, project_git_sync
import magic

from .state import (project_state_get, project_state_save, walkWithoutGit)

class Project(db.Model):
    __tablename__ = 'project'
    __table_args__ = {'sqlite_autoincrement': True}
    id = db.Column(db.Integer, primary_key=True)
    login = db.Column(db.String(60), index=True)
    name = db.Column(db.String(60), index=True)
    full_name = db.Column(db.String(60))
    html_url = db.Column(db.String(60))
    data = db.Column(db.PickleType())
    clone = db.Column(db.String(400))
    is_github = db.Column(db.Boolean(), index=True)
    is_ready = db.Column(db.Boolean(), index=True, default=False)
    # builds = db.relationship('ProjectBuild', backref='project', lazy='dynamic')

    def cache_update(self, data):
        self.html_url = data['html_url']
        self.name = data['name']
        self.data = data

    @lazy_property
    def config(self):
        # if it is not purely visible, but @lazy_property decorator cache state
        # values in runtime, when this class property acessed for the 1st time
        # it store state value. You can access it and modify, but at the end of
        # the request all modifications dies if wasn't saved
        #
        _state, _local = project_state_get(project = self)
        return {'state': _state, 'local': _local}

    def save_state(self):
        project_state_save(self)

    def setup_status(self):
        # Return project status.
        return self.config['local'].get('source', None)

    @property
    def title(self):
        """
        Return project title, resolved from repo clone if no rename family name given
        """
        # Split the git clone URL on /, take the last part, remove '.git' if it exists
        # eg "https://github.com/davelab6/league-gothic.git" -> league-gothic.git -> league-gothic
        title = self.clone.split('/')[-1].replace('.git', '')
        # If URL terminates with a /, title will be None, so take the 2nd to last part
        # eg "https://github.com/davelab6/league-gothic/" -> league-gothic
        if not title:
            title = self.clone.split('/')[-2].replace('.git', '')
        # use the family rename input if it was given
        if self.is_ready and self.config['state'].get('familyname', None):
            title = self.config['state']['familyname']
        return title

    def asset_by_name(self, name):
        """
        Resolve asset id into its real path. For internal use.

        :param name: handle for file conventionally found in repositories

        """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        if name == 'log':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.process.log' % self)
        elif name == 'yaml':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.bakery.yaml' % self)
        elif name == 'metadata':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/' % self, 'METADATA.json')
        elif name == 'metadata_new':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/' % self, 'METADATA.json.new')
        elif name == 'license':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self, self.config['state']['license_file'])
        elif name == 'description':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/' % self, 'DESCRIPTION.en_us.html')
        else:
            fn = None
        return fn

    def read_asset(self, name = None):
        fn = self.asset_by_name(name)
        if os.path.exists(fn) and os.path.isfile(fn):
            return unicode(open(fn, 'r').read(), "utf8")
        else:
            return ''

    def treeFromFilesystem(self, folder=None):
        """
        Read files tree in specied directory

        :param folder: handle for tree, either 'in' or 'out'

        Returns:
            folderContents: Dictionary of file and directory strings
        """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        _out = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/' % self)
        if folder == 'in' and os.path.exists(_in):
            folderContents = walkWithoutGit(_in)
        elif folder == 'out' and os.path.exists(_out):
            folderContents = walkWithoutGit(_out)
        else:
            folderContents = { 'Sorry, filesystem unavailable': '' }
        return folderContents

    def textFiles(self):
        """
        Read all the text files found in the _in repo

        Returns:
            textFiles: Dictionary of file and directory strings
        """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        textFiles = {}
        for textFile in self.config['local']['txt_files']:
            fn = os.path.join(_in, textFile)
            if os.path.exists(fn) and os.path.isfile(fn):
                textFiles[textFile] = unicode(open(fn, 'r').read(), "utf8")
        return textFiles

    def revision_tree(self, revision):
        """ Get specific revision files as tree in format supported by tree macros """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        d = {}
        for file in prun("git ls-tree --name-only -r %(revision)s" % locals(), cwd=_in).splitlines():
            level = d
            for part in file.split("/"):
                if part not in level:
                    level[part] = {}
                level = level[part]

        return d

    def revision_file(self, revision, fn):
        """ Read specific file from revision """
        # XXX: [xen] need review here, not sure that it is 100% safe
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        fn = fn.replace('"', '')
        # XXX: result can be tree
        data = prun('git show "%(revision)s:%(fn)s"' % locals(), cwd=_in)
        mime = magic.from_buffer(data, mime=True)
        if mime.startswith("text"):
            data = unicode(data, "utf8")
        return mime, data

    def revision_info(self, revision):
        """ Return revision info for selected git commit """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        return prun("git show --quiet --format=short %(revision)s" % locals(), cwd=_in)

    def save_asset(self, name = None, data = None, **kwarg):
        """ Save static files into out folder """
        if name == 'description':
            f = open(self.asset_by_name(name), 'w')
            f.write(data)
            f.close()
        elif name == 'metadata':
            f = open(self.asset_by_name(name), 'w')
            json.dump(json.loads(data), f, indent=2, ensure_ascii=True) # same params as in generatemetadata.py
            f.close()

            if kwarg.get('del_new') and kwarg['del_new']:
                if os.path.exists(self.asset_by_name('metadata_new')):
                    os.remove(self.asset_by_name('metadata_new'))

    @property
    def family_stat(self):
        from ..models import FontStats

        if self.config['state'].get('stats_family_name'):
            return FontStats.by_family(self.config['state']['stats_family_name'])
        elif self.config['state'].get('familyname'):
            return FontStats.by_family(self.config['state']['familyname'])
        else:
            return None

    def __getitem__(self, key):
        """ Magic method that allow to access ORM properties using
        object-dot-propertyname """
        # make magic mapping works
        return self.__dict__.get(key)

    def __len__(self):
        return len(self.__dict__.keys())

    def current_revision(self):
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        return prun("git rev-parse --short HEAD", cwd=_in).strip()

    def sync(self):
        """ Call in background git syncronization """
        project_git_sync.ctx_delay(self)

    def gitlog(self, skip=0):
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        params = "git log -n101 --skip=%(skip)s" % {'skip': skip}
        fmt = """ --pretty=format:'- {"hash":"%h", "commit":"%H","author":"%an <%ae>","date":"%ar","message": "%s"}' """
        log = prun(params + fmt, cwd = _in)
        return yaml.load(log)

    def diff_files(self, left, right):
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        diffdata = prun(""" git diff --name-only %(left)s %(right)s""" % locals(), cwd = _in)
        return diffdata

class ProjectBuild(db.Model):
    __tablename__ = 'project_build'
    __table_args__ = {'sqlite_autoincrement': True}
    id = db.Column(db.Integer, primary_key=True)
    project_id = db.Column(db.Integer, db.ForeignKey('project.id'))
    project = db.relationship(Project)
    # git rev-parse --short HEAD
    revision = db.Column(db.String(40))
    is_done = db.Column(db.Boolean(), default=False)
    created = db.Column(db.DateTime, default=datetime.now)
    updated = db.Column(db.DateTime, default=datetime.now, onupdate=db.func.now())

    @staticmethod
    def make_build(project, revision):
        """ Initiate project build process

        :param project: `Project` instance
        :param revision: git revision id
        :return: `ProjectBuild` instance
        """
        if revision == 'HEAD':
            revision = project.current_revision()
        build = ProjectBuild(project = project, revision=revision)
        db.session.add(build)
        db.session.commit()
        db.session.refresh(project)
        db.session.refresh(build)
        process_project.delay(project, build, revision)
        return build

    def utests(self):
        """ Return saved upstream test data """
        if not self.is_done:
            return {}

        param = { 'login': self.project.login, 'id': self.project.id,
            'revision': self.revision, 'build': self.id }
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _out_yaml = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/%(revision)s.%(build)s.utests.yaml' % param)
        if os.path.exists(_out_yaml):
            return yaml.load(open(_out_yaml).read())
        else:
            return {}

    def rtests(self):
        """ Return saved result test data """
        if not self.is_done:
            return {}

        param = { 'login': self.project.login, 'id': self.project.id,
            'revision': self.revision, 'build': self.id }
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _out_yaml = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/%(revision)s.%(build)s.rtests.yaml' % param)
        if os.path.exists(_out_yaml):
            return yaml.load(open(_out_yaml).read())
        else:
            return {}

<commit_msg>Model changes for #213<commit_after># coding: utf-8
# Copyright 2013 The Font Bakery Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# See AUTHORS.txt for the list of Authors and LICENSE.txt for the License.

from datetime import datetime
import os
import yaml
from flask import current_app, json
from ..decorators import lazy_property
from ..extensions import db
from ..tasks import process_project, prun, project_git_sync
import magic

from .state import (project_state_get, project_state_save, walkWithoutGit)

class Project(db.Model):
    __tablename__ = 'project'
    __table_args__ = {'sqlite_autoincrement': True}
    id = db.Column(db.Integer, primary_key=True)
    login = db.Column(db.String(60), index=True)
    name = db.Column(db.String(60), index=True)
    full_name = db.Column(db.String(60))
    html_url = db.Column(db.String(60))
    data = db.Column(db.PickleType())
    clone = db.Column(db.String(400))
    is_github = db.Column(db.Boolean(), index=True)
    is_ready = db.Column(db.Boolean(), index=True, default=False)
    # builds = db.relationship('ProjectBuild', backref='project', lazy='dynamic')

    def cache_update(self, data):
        self.html_url = data['html_url']
        self.name = data['name']
        self.data = data

    @lazy_property
    def config(self):
        # if it is not purely visible, but @lazy_property decorator cache state
        # values in runtime, when this class property acessed for the 1st time
        # it store state value. You can access it and modify, but at the end of
        # the request all modifications dies if wasn't saved
        #
        _state, _local = project_state_get(project = self)
        return {'state': _state, 'local': _local}

    def save_state(self):
        project_state_save(self)

    def setup_status(self):
        # Return project status.
        return self.config['local'].get('source', None)

    @property
    def title(self):
        """
        Return project title, resolved from repo clone if no rename family name given
        """
        # Split the git clone URL on /, take the last part, remove '.git' if it exists
        # eg "https://github.com/davelab6/league-gothic.git" -> league-gothic.git -> league-gothic
        title = self.clone.split('/')[-1].replace('.git', '')
        # If URL terminates with a /, title will be None, so take the 2nd to last part
        # eg "https://github.com/davelab6/league-gothic/" -> league-gothic
        if not title:
            title = self.clone.split('/')[-2].replace('.git', '')
        # use the family rename input if it was given
        if self.is_ready and self.config['state'].get('familyname', None):
            title = self.config['state']['familyname']
        return title

    def asset_by_name(self, name):
        """
        Resolve asset id into its real path. For internal use.

        :param name: handle for file conventionally found in repositories

        """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        if name == 'log':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.process.log' % self)
        elif name == 'yaml':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.bakery.yaml' % self)
        elif name == 'metadata':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/' % self, 'METADATA.json')
        elif name == 'metadata_new':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/' % self, 'METADATA.json.new')
        elif name == 'license':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self, self.config['state']['license_file'])
        elif name == 'description':
            fn = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/' % self, 'DESCRIPTION.en_us.html')
        else:
            fn = None
        return fn

    def read_asset(self, name = None):
        fn = self.asset_by_name(name)
        if os.path.exists(fn) and os.path.isfile(fn):
            return unicode(open(fn, 'r').read(), "utf8")
        else:
            return ''

    def treeFromFilesystem(self, folder=None):
        """
        Read files tree in specied directory

        :param folder: handle for tree, either 'in' or 'out'

        Returns:
            folderContents: Dictionary of file and directory strings
        """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        _out = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/' % self)
        if folder == 'in' and os.path.exists(_in):
            folderContents = walkWithoutGit(_in)
        elif folder == 'out' and os.path.exists(_out):
            folderContents = walkWithoutGit(_out)
        else:
            folderContents = { 'Sorry, filesystem unavailable': '' }
        return folderContents

    def textFiles(self):
        """
        Read all the text files found in the _in repo

        Returns:
            textFiles: Dictionary of file and directory strings
        """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        textFiles = {}
        for textFile in self.config['local']['txt_files']:
            fn = os.path.join(_in, textFile)
            if os.path.exists(fn) and os.path.isfile(fn):
                textFiles[textFile] = unicode(open(fn, 'r').read(), "utf8")
        return textFiles

    def revision_tree(self, revision):
        """ Get specific revision files as tree in format supported by tree macros """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        d = {}
        for file in prun("git ls-tree --name-only -r %(revision)s" % locals(), cwd=_in).splitlines():
            level = d
            for part in file.split("/"):
                if part not in level:
                    level[part] = {}
                level = level[part]

        return d

    def revision_file(self, revision, fn):
        """ Read specific file from revision """
        # XXX: [xen] need review here, not sure that it is 100% safe
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        fn = fn.replace('"', '')
        # XXX: result can be tree
        data = prun('git show "%(revision)s:%(fn)s"' % locals(), cwd=_in)
        mime = magic.from_buffer(data, mime=True)
        if mime.startswith("text"):
            data = unicode(data, "utf8")
        return mime, data

    def revision_info(self, revision):
        """ Return revision info for selected git commit """
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        return prun("git show --quiet --format=short %(revision)s" % locals(), cwd=_in)

    def save_asset(self, name = None, data = None, **kwarg):
        """ Save static files into out folder """
        if name == 'description':
            f = open(self.asset_by_name(name), 'w')
            f.write(data)
            f.close()
        elif name == 'metadata':
            f = open(self.asset_by_name(name), 'w')
            json.dump(json.loads(data), f, indent=2, ensure_ascii=True) # same params as in generatemetadata.py
            f.close()

            if kwarg.get('del_new') and kwarg['del_new']:
                if os.path.exists(self.asset_by_name('metadata_new')):
                    os.remove(self.asset_by_name('metadata_new'))

    @property
    def family_stat(self):
        from ..models import FontStats

        if self.config['state'].get('stats_family_name'):
            return FontStats.by_family(self.config['state']['stats_family_name'])
        elif self.config['state'].get('familyname'):
            return FontStats.by_family(self.config['state']['familyname'])
        else:
            return None

    def __getitem__(self, key):
        """ Magic method that allow to access ORM properties using
        object-dot-propertyname """
        # make magic mapping works
        return self.__dict__.get(key)

    def __len__(self):
        return len(self.__dict__.keys())

    def current_revision(self):
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        return prun("git rev-parse --short HEAD", cwd=_in).strip()

    def sync(self):
        """ Call in background git syncronization """
        project_git_sync.ctx_delay(self)

    def gitlog(self, skip=0):
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        params = "git log -n101 --skip=%(skip)s" % {'skip': skip}
        fmt = """ --pretty=format:'- {"hash":"%h", "commit":"%H","author":"%an <%ae>","date":"%ar","message": "%s"}' """
        log = prun(params + fmt, cwd = _in)
        return yaml.load(log)

    def diff_files(self, left, right):
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _in = os.path.join(DATA_ROOT, '%(login)s/%(id)s.in/' % self)
        diffdata = prun(""" git diff --name-only %(left)s %(right)s""" % locals(), cwd = _in)
        return diffdata

class ProjectBuild(db.Model):
    __tablename__ = 'project_build'
    __table_args__ = {'sqlite_autoincrement': True}
    id = db.Column(db.Integer, primary_key=True)
    project_id = db.Column(db.Integer, db.ForeignKey('project.id'))
    project = db.relationship(Project)
    # git rev-parse --short HEAD
    revision = db.Column(db.String(40))
    is_done = db.Column(db.Boolean(), default=False)
    created = db.Column(db.DateTime, default=datetime.now)
    modified = db.Column(db.Boolean(), default=False)
    updated = db.Column(db.DateTime, default=datetime.now, onupdate=db.func.now())

    @staticmethod
    def make_build(project, revision):
        """ Initiate project build process

        :param project: `Project` instance
        :param revision: git revision id
        :return: `ProjectBuild` instance
        """
        if revision == 'HEAD':
            revision = project.current_revision()
        build = ProjectBuild(project = project, revision=revision)
        db.session.add(build)
        db.session.commit()
        db.session.refresh(project)
        db.session.refresh(build)
        process_project.delay(project, build, revision)
        return build

    def utests(self):
        """ Return saved upstream test data """
        if not self.is_done:
            return {}

        param = { 'login': self.project.login, 'id': self.project.id,
            'revision': self.revision, 'build': self.id }
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _out_yaml = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/%(revision)s.%(build)s.utests.yaml' % param)
        if os.path.exists(_out_yaml):
            return yaml.load(open(_out_yaml).read())
        else:
            return {}

    def rtests(self):
        """ Return saved result test data """
        if not self.is_done:
            return {}

        param = { 'login': self.project.login, 'id': self.project.id,
            'revision': self.revision, 'build': self.id }
        DATA_ROOT = current_app.config.get('DATA_ROOT')
        _out_yaml = os.path.join(DATA_ROOT, '%(login)s/%(id)s.out/%(revision)s.%(build)s.rtests.yaml' % param)
        if os.path.exists(_out_yaml):
            return yaml.load(open(_out_yaml).read())
        else:
            return {}

<|endoftext|>