<commit_before>try:
    from PySide import QtCore, QtGui
    from PySide.QtCore import Qt
except ImportError:
    from PyQt4 import QtCore, QtGui
    from PyQt4.QtCore import Qt

class AutoCompleteEdit(QtGui.QLineEdit):
    """
    Based on
    http://elentok.blogspot.com/2011/08/autocomplete-textbox-for-multiple.html
    """

    def __init__(self, model):
        super(AutoCompleteEdit, self).__init__()
        self._completer = QtGui.QCompleter(model)
        self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self._completer.setWidget(self)
        self._completer.activated.connect(self._insertCompletion)

        self._keysToIgnore = [QtCore.Qt.Key_Enter,
                              QtCore.Qt.Key_Return,
                              #QtCore.Qt.Key_Escape,
                              QtCore.Qt.Key_Tab]

    def _insertCompletion(self, completion):
        """
        This is the event handler for the QCompleter.activated(QString) signal,
        it is called when the user selects an item in the completer popup.
        """
        # Could potentially use self._completer.completionPrefix() to
        # append only the remained of the word, but it's more useful
        # to wipe all other text for this
        self.setText(completion)

    def textUnderCursor(self):
        text = self.text()
        i = self.cursorPosition()
        before = text[:i]
        #after = text[i:]
        return before

    def keyPressEvent(self, event):
        if self._completer.popup().isVisible():
            if event.key() in self._keysToIgnore:
                event.ignore()
                return

        super(AutoCompleteEdit, self).keyPressEvent(event)
        completionPrefix = self.textUnderCursor()

        if completionPrefix != self._completer.completionPrefix():
            self._updateCompleterPopupItems(completionPrefix)

        if len(event.text()) > 0 and len(completionPrefix) > 0:
            self._completer.complete()

        if len(completionPrefix) == 0:
            self._completer.popup().hide()

    def _updateCompleterPopupItems(self, completionPrefix):
        """
        Filters the completer's popup items to only show items
        with the given prefix.
        """
        self._completer.setCompletionPrefix(completionPrefix)
        self._completer.popup().setCurrentIndex(
                self._completer.completionModel().index(0,0))

def find_menu_items(menu, _path = None):
    """Extracts items from a given Nuke menu

    Returns a list of strings, with the path to each item

    Ignores divider lines and hidden items (ones like "@;&CopyBranch" for shift+k)

    >>> found = find_menu_items(nuke.menu("Nodes"))
    >>> found.sort()
    >>> found[:5]
    ['/3D/Axis', '/3D/Camera', '/3D/CameraTracker', '/3D/DepthGenerator', '/3D/Geometry/Card']
    """
    import nuke

    if _path is None:
        _path = ""

    found = []

    mi = menu.items()
    for i in mi:
        if isinstance(i, nuke.Menu):
            # Sub-menu, recurse
            mname = i.name().replace("&", "")
            sub_found = find_menu_items(menu = i, _path = "%s/%s" % (_path, mname))
            found.extend(sub_found)
        elif isinstance(i, nuke.MenuItem):
            if i.name() == "":
                # Skip dividers
                continue
            if i.name().startswith("@;"):
                # Skip hidden items
                continue
            found.append("%s/%s" % (_path, i.name()))

    return found

_widget = None
def nukedemo():
    global _widget

    import nuke
    values = find_menu_items(nuke.menu("Nodes"))

    hbox = QtGui.QHBoxLayout()

    print dir(hbox)
    #hbox.setSpacing(0)
    hbox.setContentsMargins(0, 0, 0, 0)

    editor = AutoCompleteEdit(values)
    hbox.addWidget(editor)

    window = QtGui.QWidget(None)#, Qt.FramelessWindowHint)
    window.setLayout(hbox)

    def yay(word):
        print "yay", word
        nuke.menu("Nodes").findItem(word.lstrip("/")).invoke()
        window.hide()
        window.destroy()

    editor._completer.activated.connect(yay)
    window.show()
    _widget = window

if __name__ == '__main__':
    def demo():
        import random
        values = [x.strip() for x in random.sample(open("/usr/share/dict/words").readlines(), 1000)]

        import sys
        app = QtGui.QApplication(sys.argv)
        editor = AutoCompleteEdit(values)

        window = QtGui.QWidget(None)#, Qt.FramelessWindowHint)

        def yay(word):
            print "yay", repr(word)
            editor.hide()

        editor._completer.activated.connect(yay)

        editor.show()
        editor.raise_()

        sys.exit(app.exec_())

    demo()
<commit_msg>Remove old code<commit_after><|endoftext|>


<commit_before># -*- coding: utf-8 -*-
from __future__ import unicode_literals

import logging

from os.path import join
from pkg_resources import resource_isdir, resource_listdir, resource_string

from flask import current_app

from pymongo.errors import PyMongoError, OperationFailure
from flask_mongoengine.connection import get_db
from flask_mongoengine.connection import DEFAULT_CONNECTION_NAME

from udata.commands import submanager, green, yellow, cyan, purple, red

log = logging.getLogger(__name__)

m = submanager(
    'db',
    help='Database related operations',
    description='Handle all database related operations and maintenance'
)

# A migration script wrapper recording the stdout lines
SCRIPT_WRAPPER = '''
function(plugin, filename, script) {{
    var stdout = [];
    function print() {{
        var args = Array.prototype.slice.call(arguments);
        stdout.push(args.join(' '));
    }}

    {0}

    db.migrations.insert({{
        plugin: plugin,
        filename: filename,
        date: ISODate(),
        script: script,
        output: stdout.join('\\n')
    }});

    return stdout;
}}
'''

# Only record a migration script
RECORD_WRAPPER = '''
function(plugin, filename, script) {
    db.migrations.insert({
        plugin: plugin,
        filename: filename,
        date: ISODate(),
        script: script,
        output: 'Recorded only'
    });
}
'''

# Only record a migration script
UNRECORD_WRAPPER = '''
function(oid) {{
    db.migrations.remove({_id: oid});
}}
'''

# Date format used to for display
DATE_FORMAT = '%Y-%m-%d %H:%M'

def get_migration(plugin, filename):
    '''Get an existing migration record if exists'''
    db = get_db(DEFAULT_CONNECTION_NAME)
    return db.migrations.find_one({'plugin': plugin, 'filename': filename})

def execute_migration(plugin, filename, script, dryrun=False):
    '''Execute and record a migration'''
    db = get_db(DEFAULT_CONNECTION_NAME)
    js = SCRIPT_WRAPPER.format(script)
    lines = script.splitlines()
    success = True
    if not dryrun:
        try:
            lines = db.eval(js, plugin, filename, script)
        except OperationFailure as e:
            log.error(e.details['errmsg'].replace('\n', '\\n'))
            success = False
        except PyMongoError as e:
            log.error('Unable to apply migration: %s', str(e))
            success = False
    print('│')
    for line in lines:
        print('│ {0}'.format(line))
    print('│')
    print('└──[{0}]'.format(green('OK') if success else red('KO')))
    print('')
    return success

def record_migration(plugin, filename, script, **kwargs):
    '''Only record a migration without applying it'''
    db = get_db(DEFAULT_CONNECTION_NAME)
    db.eval(RECORD_WRAPPER, plugin, filename, script)
    return True

def available_migrations():
    '''
    List available migrations for udata and enabled plugins

    Each row is tuple with following signature:

        (plugin, package, filename)
    '''
    migrations = []
    for filename in resource_listdir('udata', 'migrations'):
        if filename.endswith('.js'):
            migrations.append(('udata', 'udata', filename))

    for plugin in current_app.config['PLUGINS']:
        name = 'udata_{0}'.format(plugin)
        if resource_isdir(name, 'migrations'):
            for filename in resource_listdir(name, 'migrations'):
                if filename.endswith('.js'):
                    migrations.append((plugin, name, filename))
    return sorted(migrations, key=lambda r: r[2])

def log_status(plugin, filename, status):
    '''Properly display a migration status line'''
    display = ':'.join((plugin, filename)) + ' '
    log.info('%s [%s]', '{:.<70}'.format(display), status)

@m.command
def status():
    '''Display the database migrations status'''
    for plugin, package, filename in available_migrations():
        migration = get_migration(plugin, filename)
        if migration:
            status = green(migration['date'].strftime(DATE_FORMAT))
        else:
            status = yellow('Not applied')
        log_status(plugin, filename, status)

@m.option('-r', '--record', action='store_true',
          help='Only records the migrations')
@m.option('-d', '--dry-run', action='store_true', dest='dryrun',
          help='Only print migrations to be applied')
def migrate(record, dryrun=False):
    '''Perform database migrations'''
    handler = record_migration if record else execute_migration
    success = True
    for plugin, package, filename in available_migrations():
        migration = get_migration(plugin, filename)
        if migration or not success:
            log_status(plugin, filename, cyan('Skipped'))
        else:
            status = purple('Recorded') if record else yellow('Apply')
            log_status(plugin, filename, status)
            script = resource_string(package, join('migrations', filename))
            success &= handler(plugin, filename, script, dryrun=dryrun)

@m.command
def unrecord(plugin, filename):
    '''Remove a database migration record'''
    migration = get_migration(plugin, filename)
    if migration:
        log.info('Removing migration %s:%s', plugin, filename)
        db = get_db(DEFAULT_CONNECTION_NAME)
        db.eval(UNRECORD_WRAPPER, migration['_id'])
    else:
        log.error('Migration not found %s:%s', plugin, filename)
<commit_msg>Adapt DB commands to new connection manager<commit_after># -*- coding: utf-8 -*-
from __future__ import unicode_literals

import logging

from os.path import join
from pkg_resources import resource_isdir, resource_listdir, resource_string

from flask import current_app

from pymongo.errors import PyMongoError, OperationFailure
from mongoengine.connection import get_db

from udata.commands import submanager, green, yellow, cyan, purple, red

log = logging.getLogger(__name__)

m = submanager(
    'db',
    help='Database related operations',
    description='Handle all database related operations and maintenance'
)

# A migration script wrapper recording the stdout lines
SCRIPT_WRAPPER = '''
function(plugin, filename, script) {{
    var stdout = [];
    function print() {{
        var args = Array.prototype.slice.call(arguments);
        stdout.push(args.join(' '));
    }}

    {0}

    db.migrations.insert({{
        plugin: plugin,
        filename: filename,
        date: ISODate(),
        script: script,
        output: stdout.join('\\n')
    }});

    return stdout;
}}
'''

# Only record a migration script
RECORD_WRAPPER = '''
function(plugin, filename, script) {
    db.migrations.insert({
        plugin: plugin,
        filename: filename,
        date: ISODate(),
        script: script,
        output: 'Recorded only'
    });
}
'''

# Only record a migration script
UNRECORD_WRAPPER = '''
function(oid) {{
    db.migrations.remove({_id: oid});
}}
'''

# Date format used to for display
DATE_FORMAT = '%Y-%m-%d %H:%M'

def get_migration(plugin, filename):
    '''Get an existing migration record if exists'''
    db = get_db()
    return db.migrations.find_one({'plugin': plugin, 'filename': filename})

def execute_migration(plugin, filename, script, dryrun=False):
    '''Execute and record a migration'''
    db = get_db()
    js = SCRIPT_WRAPPER.format(script)
    lines = script.splitlines()
    success = True
    if not dryrun:
        try:
            lines = db.eval(js, plugin, filename, script)
        except OperationFailure as e:
            log.error(e.details['errmsg'].replace('\n', '\\n'))
            success = False
        except PyMongoError as e:
            log.error('Unable to apply migration: %s', str(e))
            success = False
    print('│')
    for line in lines:
        print('│ {0}'.format(line))
    print('│')
    print('└──[{0}]'.format(green('OK') if success else red('KO')))
    print('')
    return success

def record_migration(plugin, filename, script, **kwargs):
    '''Only record a migration without applying it'''
    db = get_db()
    db.eval(RECORD_WRAPPER, plugin, filename, script)
    return True

def available_migrations():
    '''
    List available migrations for udata and enabled plugins

    Each row is tuple with following signature:

        (plugin, package, filename)
    '''
    migrations = []
    for filename in resource_listdir('udata', 'migrations'):
        if filename.endswith('.js'):
            migrations.append(('udata', 'udata', filename))

    for plugin in current_app.config['PLUGINS']:
        name = 'udata_{0}'.format(plugin)
        if resource_isdir(name, 'migrations'):
            for filename in resource_listdir(name, 'migrations'):
                if filename.endswith('.js'):
                    migrations.append((plugin, name, filename))
    return sorted(migrations, key=lambda r: r[2])

def log_status(plugin, filename, status):
    '''Properly display a migration status line'''
    display = ':'.join((plugin, filename)) + ' '
    log.info('%s [%s]', '{:.<70}'.format(display), status)

@m.command
def status():
    '''Display the database migrations status'''
    for plugin, package, filename in available_migrations():
        migration = get_migration(plugin, filename)
        if migration:
            status = green(migration['date'].strftime(DATE_FORMAT))
        else:
            status = yellow('Not applied')
        log_status(plugin, filename, status)

@m.option('-r', '--record', action='store_true',
          help='Only records the migrations')
@m.option('-d', '--dry-run', action='store_true', dest='dryrun',
          help='Only print migrations to be applied')
def migrate(record, dryrun=False):
    '''Perform database migrations'''
    handler = record_migration if record else execute_migration
    success = True
    for plugin, package, filename in available_migrations():
        migration = get_migration(plugin, filename)
        if migration or not success:
            log_status(plugin, filename, cyan('Skipped'))
        else:
            status = purple('Recorded') if record else yellow('Apply')
            log_status(plugin, filename, status)
            script = resource_string(package, join('migrations', filename))
            success &= handler(plugin, filename, script, dryrun=dryrun)

@m.command
def unrecord(plugin, filename):
    '''Remove a database migration record'''
    migration = get_migration(plugin, filename)
    if migration:
        log.info('Removing migration %s:%s', plugin, filename)
        db = get_db()
        db.eval(UNRECORD_WRAPPER, migration['_id'])
    else:
        log.error('Migration not found %s:%s', plugin, filename)
<|endoftext|>


<commit_before>"""
DrinkRecipe class encapsulates how a drink recipe is calculated and formulated,
can provide itself as a dict/json, tuple of values, do conversions, etc.
Just generally make it better OOP
"""
import re
from fractions import Fraction
from recordtype import recordtype
import itertools
import string

import util

# water volume added by preperation method for ABV estimate
# TODO by ice and prep instead -e.g. build, highball, cubed vs build, flute, neat
WATER_BY_PREP = {'shake': 1.65, 'stir': 1.3, 'build': 1.0}

class RecipeError(StandardError):
    pass

class DrinkRecipe(object):
    """ Initialize a drink with a handle to the available stock data and its recipe json
    """
    RecipeExample = recordtype('RecipeExample', [('bottles', []), ('cost', 0), ('abv', 0), ('std_drinks', 0), ('volume', 0)])
    RecipeStats = recordtype('RecipeStats', 'min_cost,max_cost,min_abv,max_abv,min_std_drinks,max_std_drinks,volume', default=RecipeExample)

    @util.default_initializer
    def __init__(self, name, recipe_dict, stock_df=None):
        # from recipe dict pull out other info and set defaults
        self.info      =  recipe_dict.get('info', '')
        self.style     =  recipe_dict.get('style', '')
        self.tag       =  recipe_dict.get('tag', '')
        self.iba_info  =  recipe_dict.get('IBA_description', '')
        self.origin    =  recipe_dict.get('origin', '')
        self.unit      =  recipe_dict.get('unit', 'oz') # cL, mL, tsp, dash, drop
        self.prep      =  recipe_dict.get('prep', 'shake') # build, stir, blend
        self.ice       =  recipe_dict.get('ice', 'cubed') # crushed, neat
        self.glass     =  recipe_dict.get('glass', 'cocktail') # rocks, martini, flute, collins, highball
        self.variants  =  recipe_dict.get('variants',  [])
        self.max_cost     =  0
        self.examples     =  []
        self.ingredients  =  []
        for type_str, quantity in recipe_dict.get('ingredients', {}).iteritems():
            self.ingredients.append(QuantizedIngredient(type_str, quantity, self.unit))
        for type_str, quantity in recipe_dict.get('optional', {}).iteritems():
            self.ingredients.append(OptionalIngredient(type_str, quantity, self.unit))
        if recipe_dict.get('misc'):
            self.ingredients.append(Ingredient(recipe_dict.get('misc')))
        if recipe_dict.get('garnish'):
            self.ingredients.append(Garnish(recipe_dict.get('garnish')))

        self.show_examples = False

    def __str__(self):
        """ Drink recipe basic plain text output format
        """
        lines = []
        lines.append(self.name)
        lines.extend([i.str() for i in self.ingredients])
        if self.variants:
            lines.append("\tVariants:")
            lines.extend(['\t'+v for v in self.variants])
        if self.show_examples:
            lines.append("\tExamples:")
            lines.extend(['\t'+v for v in self.examples])
        lines.append('')
        return '\n'.join(lines)

    def __repr__(self):
        return "{}:{}".format(self.__class__.__name__, self.name)

    def prep_line(self, extended=True, caps=True):
        case_fn = string.upper if caps else string.lower
        layout = "{{}} glass | {{}}{} | {{}}".format("" if self.ice == 'neat' else " ice") if extended else "{} | {} | {}"
        return case_fn(layout.format(self.glass, self.ice, self.prep))

    @property
    def can_make(self):
        return bool(self.examples)

    def convert(self, to_unit, rounded=True, convert_nonstandard=False):
        """ Convert the main unit of this recipe
        """
        if self.unit == to_unit:
            return
        for ingredient in self.ingredients:
            ingredient.recipe_unit = to_unit
            if ingredient.unit in ['ds', 'drop'] and not convert_nonstandard:
                continue
            try:
                ingredient.convert(to_unit, rounded=rounded)
            except NotImplementedError:
                pass
        self.unit = to_unit

    def generate_examples(self, barstock):
        """ Given a Barstock, calculate examples drinks from the data
        e.g. For every dry gin and vermouth in Barstock, generate every Martini
        that can be made, along with the cost,abv,std_drinks from the ingredients
        """
        ingredients = self._get_quantized_ingredients()
        example_bottles = barstock.get_all_bottle_combinations((i.specifier for i in ingredients))
        del self.examples # need to make possible to run again
        self.examples = []
        for bottles in example_bottles:
            # TODO refactor to generate IngredientSpecifiers for the bottle lists
            example = DrinkRecipe.RecipeExample(); example.bottles = []
            for bottle, ingredient in zip(bottles, ingredients):
                if ingredient.unit == 'literal':
                    continue
                example.cost       += ingredient.get_cost(bottle, barstock)
                example.std_drinks += ingredient.get_std_drinks(bottle, barstock)
                example.volume     += ingredient.get_amount_as(self.unit, rounded=False, single_value=True)
                # remove juice and such from the bottles listed
                if barstock.get_bottle_category(util.IngredientSpecifier(ingredient.specifier.what, bottle)) in ['Vermouth', 'Liqueur', 'Bitters', 'Spirit', 'Wine']:
                    example.bottles.append(bottle)
            example.bottles = ', '.join(example.bottles);
            example.volume *= WATER_BY_PREP.get(self.prep, 1.0)
            example.abv = util.calculate_abv(example.std_drinks, example.volume, self.unit)
            self.max_cost = max(self.max_cost, example.cost)
            self.examples.append(example)
        return self # so it can be used when chained

    def calculate_stats(self):
        """ After generating examples, calculate stats for this drink
        """
        if not self.examples:
            return False
        def _find_example(examples, attr, max_=False):
            return sorted(examples, key=lambda e: getattr(e, attr), reverse=max_)[0]
        self.stats = self.RecipeStats()
        self.stats.min_cost = _find_example(self.examples, 'cost')
        self.stats.max_cost = _find_example(self.examples, 'cost', max_=True)
        self.stats.min_abv = _find_example(self.examples, 'abv')
        self.stats.max_abv = _find_example(self.examples, 'abv', max_=True)
        self.stats.min_std_drinks = _find_example(self.examples, 'std_drinks')
        self.stats.max_std_drinks = _find_example(self.examples, 'std_drinks', max_=True)
        self.stats.volume = _find_example(self.examples, 'volume', max_=True).volume
        return True

    def get_ingredient_list(self, all=True, optional=False):
        if all:
            return [i.specifier.what for i in self.ingredients]
        else:
            return [i.specifier.what for i in self._get_quantized_ingredients(include_optional=optional)]

    def primary_spirit(self):
        max_amount = 0
        max_ingredient = None
        for i in self._get_quantized_ingredients(): # TODO enforce ingredient Category tags
            amount = i.get_amount_as(self.unit, rounded=False, single_value=True)
            if amount > max_amount and not i.top_with:
                max_amount = amount
                max_ingredient = i.specifier.what
        return max_ingredient

    def first_ingredient(self):
        return self.ingredients[0].specifier

    def contains_ingredient(self, ingredient, include_optional=False):
        ingredient = ingredient.lower()
        return any(( ingredient in i.specifier.what.lower() \
                or (i.specifier.bottle and ingredient in i.specifier.bottle.lower()) \
                for i in self._get_quantized_ingredients(include_optional=include_optional)))

    def _get_quantized_ingredients(self, include_optional=False):
        return [i for i in self.ingredients if isinstance(i, QuantizedIngredient) \
                and (not isinstance(i, OptionalIngredient) or include_optional)]

class Ingredient(object):
    """ An "ingredient" is every item that should be represented in standard text
    """
    def _repr_fmt(self):
        return "<{}[{{}}]>".format(self.__class__.__name__)

    @util.default_initializer
    def __init__(self, description):
        self.unit = None
        self.specifier = util.IngredientSpecifier(description)

    def str(self):
        return str(self.description)

    def __repr__(self):
        return self._repr_fmt().format(self.description)

    def convert(self, *args, **kwargs):
        pass

class Garnish(Ingredient):
    """ An ingredient line that denotes it's a garnish
    """
    def str(self):
        return "{}, for garnish".format(super(Garnish, self).str())

    def __repr__(self):
        return super(Garnish, self)._repr_fmt().format(self.description)

class QuantizedIngredient(Ingredient):
    """ Has a unit based on the raw quantity, responsible for unit conversions, text output
    type_str: as written in the recipe, may be in the form what:bottle
    what: identify an ingredient, e.g. rye whiskey
    bottle: specify an ingredient, e.g. Bulliet Rye
    """
    @util.default_initializer
    def __init__(self, type_str, raw_quantity, recipe_unit):
        self.top_with = False
        self.specifier = util.IngredientSpecifier.from_string(type_str)

        # interpret the raw quantity
        if isinstance(raw_quantity, basestring):
            if raw_quantity == 'Top with': # counts at 3 ounces on average
                self.amount = util.convert_units(3.0, 'oz', recipe_unit, rounded=True)
                self.unit = recipe_unit
                self.top_with = True

            elif 'dash' in raw_quantity:
                self.unit = 'ds'
                if raw_quantity == 'dash':
                    self.amount = 1
                elif re.match(r'[0-9]+ dashes', raw_quantity):
                    self.amount = int(raw_quantity.split()[0])
                elif re.match(r'[0-9]+ to [0-9]+ dashes', raw_quantity):
                    self.amount = (int(raw_quantity.split()[0]), int(raw_quantity.split()[2]))
                else:
                    raise RecipeError("Unknown format for dash amount: {} {}".format(raw_quantity, type_str))

            elif 'tsp' in raw_quantity:
                try:
                    self.amount = float(raw_quantity.split()[0])
                except ValueError:
                    self.amount = float(Fraction(raw_quantity.split()[0]))
                self.unit = 'tsp'

            elif 'drop' in raw_quantity:
                self.unit = 'drop'
                if raw_quantity == 'drop':
                    self.amount = 1
                elif re.match(r'[0-9]+ drops', raw_quantity):
                    self.amount = int(raw_quantity.split()[0])
                else:
                    raise RecipeError("Unknown format for drop amount: {} {}".format(raw_quantity, type_str))

            elif raw_quantity in ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight']:
                self.amount = raw_quantity
                self.unit = 'literal'
            else:
                raise RecipeError("Unknown ingredient quantity: {} {}".format(raw_quantity, type_str))
        else:
            self.amount = raw_quantity
            self.unit = recipe_unit

    def __repr__(self):
        return super(QuantizedIngredient, self)._repr_fmt().format("{},{},{}".format(self.amount, self.unit, self.specifier))

    def convert(self, new_unit, rounded=True):
        if self.unit == 'literal':
            return
        self.amount = self.get_amount_as(new_unit, rounded=rounded)
        self.unit = new_unit

    def get_amount_as(self, new_unit, rounded=True, single_value=False):
        if self.unit == 'literal':
            return
        if isinstance(self.amount, tuple):
            lower  = util.convert_units(self.amount[0], self.unit, new_unit, rounded=rounded)
            higher = util.convert_units(self.amount[1], self.unit, new_unit, rounded=rounded)
            amount = (lower+higher)/2.0 if single_value else (lower, higher)
        else:
            amount = util.convert_units(self.amount, self.unit, new_unit, rounded=rounded)
        return amount

    def str(self):
        if self.top_with:
            return "Top with {}".format(self.specifier)
        if self.unit == 'literal':
            return "{} {}".format(self.amount, self.specifier)

        if self.unit == 'ds':
            if isinstance(self.amount, tuple):
                amount = "{:.0f} to {:.0f}".format(self.amount[0], self.amount[1])
                unit = 'dashes'
            elif self.amount == 1:
                amount = 'dash'
                unit = 'of'
            else:
                amount = "{:.0f}".format(self.amount)
                unit = 'dashes'
        elif self.unit == 'drop':
            if isinstance(self.amount, tuple):
                amount = "{:.0f} to {:.0f}".format(self.amount[0], self.amount[1])
                unit = 'drops'
            elif self.amount == 1:
                amount = 'drop'
                unit = 'of'
            else:
                amount = "{:.0f}".format(self.amount)
                unit = 'drops'
        elif self.unit in ['oz', 'tsp']:
            amount = util.to_fraction(self.amount)
            unit = self.unit
        elif self.unit == 'mL':
            unit = self.unit
            if self.amount < 10.0:
                amount = "{:.1f}".format(self.amount)
            else:
                amount = "{:.0f}".format(self.amount)
        else:
            amount = self.amount
            unit = self.unit

        formats = {
                #'mL': u"{:.0f} {} {}",
                'cL': u"{:.1f} {} {}",
                }
        return formats.get(self.unit, u"{} {} {}").format(amount, unit, self.specifier)

    def get_cost(self, bottle, barstock):
        if self.unit == 'literal':
            return 0
        amount = self.get_amount_as(self.recipe_unit, rounded=False, single_value=True)
        return barstock.cost_by_bottle_and_volume(util.IngredientSpecifier(self.specifier.what, bottle), amount, self.recipe_unit)

    def get_std_drinks(self, bottle, barstock):
        if self.unit == 'literal':
            return 0
        amount = self.get_amount_as(self.recipe_unit, rounded=False, single_value=True)
        proof = barstock.get_bottle_proof(util.IngredientSpecifier(self.specifier.what, bottle))
        return util.calculate_std_drinks(proof, amount, self.recipe_unit)

class OptionalIngredient(QuantizedIngredient):
    """ A quantized ingredient that just gets an extra output tag
    """
    def str(self):
        return "{}, (optional)".format(super(OptionalIngredient, self).str())

<commit_msg>Support for stats<commit_after>"""
DrinkRecipe class encapsulates how a drink recipe is calculated and formulated,
can provide itself as a dict/json, tuple of values, do conversions, etc.
Just generally make it better OOP
"""
import re
from fractions import Fraction
from recordtype import recordtype
import itertools
import string

import util

# water volume added by preperation method for ABV estimate
# TODO by ice and prep instead -e.g. build, highball, cubed vs build, flute, neat
WATER_BY_PREP = {'shake': 1.65, 'stir': 1.3, 'build': 1.0}

class RecipeError(StandardError):
    pass

class DrinkRecipe(object):
    """ Initialize a drink with a handle to the available stock data and its recipe json
    """
    RecipeExample = recordtype('RecipeExample', [('bottles', []), ('cost', 0), ('abv', 0), ('std_drinks', 0), ('volume', 0)])
    RecipeStats = recordtype('RecipeStats', 'min_cost,max_cost,min_abv,max_abv,min_std_drinks,max_std_drinks,avg_abv,avg_cost,avg_std_drinks,volume', default=RecipeExample)

    @util.default_initializer
    def __init__(self, name, recipe_dict, stock_df=None):
        # from recipe dict pull out other info and set defaults
        self.info      =  recipe_dict.get('info', '')
        self.style     =  recipe_dict.get('style', '')
        self.tag       =  recipe_dict.get('tag', '')
        self.iba_info  =  recipe_dict.get('IBA_description', '')
        self.origin    =  recipe_dict.get('origin', '')
        self.unit      =  recipe_dict.get('unit', 'oz') # cL, mL, tsp, dash, drop
        self.prep      =  recipe_dict.get('prep', 'shake') # build, stir, blend
        self.ice       =  recipe_dict.get('ice', 'cubed') # crushed, neat
        self.glass     =  recipe_dict.get('glass', 'cocktail') # rocks, martini, flute, collins, highball
        self.variants  =  recipe_dict.get('variants',  [])
        self.max_cost     =  0
        self.examples     =  []
        self.ingredients  =  []
        self.stats = None
        for type_str, quantity in recipe_dict.get('ingredients', {}).iteritems():
            self.ingredients.append(QuantizedIngredient(type_str, quantity, self.unit))
        for type_str, quantity in recipe_dict.get('optional', {}).iteritems():
            self.ingredients.append(OptionalIngredient(type_str, quantity, self.unit))
        if recipe_dict.get('misc'):
            self.ingredients.append(Ingredient(recipe_dict.get('misc')))
        if recipe_dict.get('garnish'):
            self.ingredients.append(Garnish(recipe_dict.get('garnish')))

        self.show_examples = False

    def __str__(self):
        """ Drink recipe basic plain text output format
        """
        lines = []
        lines.append(self.name)
        lines.extend([i.str() for i in self.ingredients])
        if self.variants:
            lines.append("\tVariants:")
            lines.extend(['\t'+v for v in self.variants])
        if self.show_examples:
            lines.append("\tExamples:")
            lines.extend(['\t'+v for v in self.examples])
        lines.append('')
        return '\n'.join(lines)

    def __repr__(self):
        return "{}:{}".format(self.__class__.__name__, self.name)

    def prep_line(self, extended=True, caps=True):
        case_fn = string.upper if caps else string.lower
        layout = "{{}} glass | {{}}{} | {{}}".format("" if self.ice == 'neat' else " ice") if extended else "{} | {} | {}"
        return case_fn(layout.format(self.glass, self.ice, self.prep))

    @property
    def can_make(self):
        return bool(self.examples)

    def convert(self, to_unit, rounded=True, convert_nonstandard=False):
        """ Convert the main unit of this recipe
        """
        if self.unit == to_unit:
            return
        for ingredient in self.ingredients:
            ingredient.recipe_unit = to_unit
            if ingredient.unit in ['ds', 'drop'] and not convert_nonstandard:
                continue
            try:
                ingredient.convert(to_unit, rounded=rounded)
            except NotImplementedError:
                pass
        self.unit = to_unit

    def generate_examples(self, barstock, stats=False):
        """ Given a Barstock, calculate examples drinks from the data
        e.g. For every dry gin and vermouth in Barstock, generate every Martini
        that can be made, along with the cost,abv,std_drinks from the ingredients
        """
        ingredients = self._get_quantized_ingredients()
        example_bottles = barstock.get_all_bottle_combinations((i.specifier for i in ingredients))
        del self.examples # need to make possible to run again
        self.examples = []
        for bottles in example_bottles:
            # TODO refactor to generate IngredientSpecifiers for the bottle lists
            example = DrinkRecipe.RecipeExample(); example.bottles = []
            for bottle, ingredient in zip(bottles, ingredients):
                if ingredient.unit == 'literal':
                    continue
                example.cost       += ingredient.get_cost(bottle, barstock)
                example.std_drinks += ingredient.get_std_drinks(bottle, barstock)
                example.volume     += ingredient.get_amount_as(self.unit, rounded=False, single_value=True)
                # remove juice and such from the bottles listed
                if barstock.get_bottle_category(util.IngredientSpecifier(ingredient.specifier.what, bottle)) in ['Vermouth', 'Liqueur', 'Bitters', 'Spirit', 'Wine']:
                    example.bottles.append(bottle)
            example.bottles = ', '.join(example.bottles);
            example.volume *= WATER_BY_PREP.get(self.prep, 1.0)
            example.abv = util.calculate_abv(example.std_drinks, example.volume, self.unit)
            self.max_cost = max(self.max_cost, example.cost)
            self.examples.append(example)
        if stats:
            self.calculate_stats()
        return self # so it can be used when chained

    def calculate_stats(self):
        """ After generating examples, calculate stats for this drink
        """
        if not self.examples:
            return False
        if self.stats:
            return True
        def _find_example(examples, attr, max_=False):
            return sorted(examples, key=lambda e: getattr(e, attr), reverse=max_)[0]
        def _mean(examples, attr):
            return sum((getattr(e, attr) for e in examples)) / float(len(examples))
        self.stats = self.RecipeStats()
        self.stats.min_cost = _find_example(self.examples, 'cost')
        self.stats.max_cost = _find_example(self.examples, 'cost', max_=True)
        self.stats.min_abv = _find_example(self.examples, 'abv')
        self.stats.max_abv = _find_example(self.examples, 'abv', max_=True)
        self.stats.min_std_drinks = _find_example(self.examples, 'std_drinks')
        self.stats.max_std_drinks = _find_example(self.examples, 'std_drinks', max_=True)
        self.stats.volume = _find_example(self.examples, 'volume', max_=True).volume
        self.stats.avg_cost = _mean(self.examples, 'cost')
        self.stats.avg_abv = _mean(self.examples, 'abv')
        self.stats.avg_std_drinks = _mean(self.examples, 'std_drinks')
        return True

    def get_ingredient_list(self, all=True, optional=False):
        if all:
            return [i.specifier.what for i in self.ingredients]
        else:
            return [i.specifier.what for i in self._get_quantized_ingredients(include_optional=optional)]

    def primary_spirit(self):
        max_amount = 0
        max_ingredient = None
        for i in self._get_quantized_ingredients(): # TODO enforce ingredient Category tags
            amount = i.get_amount_as(self.unit, rounded=False, single_value=True)
            if amount > max_amount and not i.top_with:
                max_amount = amount
                max_ingredient = i.specifier.what
        return max_ingredient

    def first_ingredient(self):
        return self.ingredients[0].specifier

    def contains_ingredient(self, ingredient, include_optional=False):
        ingredient = ingredient.lower()
        return any(( ingredient in i.specifier.what.lower() \
                or (i.specifier.bottle and ingredient in i.specifier.bottle.lower()) \
                for i in self._get_quantized_ingredients(include_optional=include_optional)))

    def _get_quantized_ingredients(self, include_optional=False):
        return [i for i in self.ingredients if isinstance(i, QuantizedIngredient) \
                and (not isinstance(i, OptionalIngredient) or include_optional)]

class Ingredient(object):
    """ An "ingredient" is every item that should be represented in standard text
    """
    def _repr_fmt(self):
        return "<{}[{{}}]>".format(self.__class__.__name__)

    @util.default_initializer
    def __init__(self, description):
        self.unit = None
        self.specifier = util.IngredientSpecifier(description)

    def str(self):
        return str(self.description)

    def __repr__(self):
        return self._repr_fmt().format(self.description)

    def convert(self, *args, **kwargs):
        pass

class Garnish(Ingredient):
    """ An ingredient line that denotes it's a garnish
    """
    def str(self):
        return "{}, for garnish".format(super(Garnish, self).str())

    def __repr__(self):
        return super(Garnish, self)._repr_fmt().format(self.description)

class QuantizedIngredient(Ingredient):
    """ Has a unit based on the raw quantity, responsible for unit conversions, text output
    type_str: as written in the recipe, may be in the form what:bottle
    what: identify an ingredient, e.g. rye whiskey
    bottle: specify an ingredient, e.g. Bulliet Rye
    """
    @util.default_initializer
    def __init__(self, type_str, raw_quantity, recipe_unit):
        self.top_with = False
        self.specifier = util.IngredientSpecifier.from_string(type_str)

        # interpret the raw quantity
        if isinstance(raw_quantity, basestring):
            if raw_quantity == 'Top with': # counts at 3 ounces on average
                self.amount = util.convert_units(3.0, 'oz', recipe_unit, rounded=True)
                self.unit = recipe_unit
                self.top_with = True

            elif 'dash' in raw_quantity:
                self.unit = 'ds'
                if raw_quantity == 'dash':
                    self.amount = 1
                elif re.match(r'[0-9]+ dashes', raw_quantity):
                    self.amount = int(raw_quantity.split()[0])
                elif re.match(r'[0-9]+ to [0-9]+ dashes', raw_quantity):
                    self.amount = (int(raw_quantity.split()[0]), int(raw_quantity.split()[2]))
                else:
                    raise RecipeError("Unknown format for dash amount: {} {}".format(raw_quantity, type_str))

            elif 'tsp' in raw_quantity:
                try:
                    self.amount = float(raw_quantity.split()[0])
                except ValueError:
                    self.amount = float(Fraction(raw_quantity.split()[0]))
                self.unit = 'tsp'

            elif 'drop' in raw_quantity:
                self.unit = 'drop'
                if raw_quantity == 'drop':
                    self.amount = 1
                elif re.match(r'[0-9]+ drops', raw_quantity):
                    self.amount = int(raw_quantity.split()[0])
                else:
                    raise RecipeError("Unknown format for drop amount: {} {}".format(raw_quantity, type_str))

            elif raw_quantity in ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight']:
                self.amount = raw_quantity
                self.unit = 'literal'
            else:
                raise RecipeError("Unknown ingredient quantity: {} {}".format(raw_quantity, type_str))
        else:
            self.amount = raw_quantity
            self.unit = recipe_unit

    def __repr__(self):
        return super(QuantizedIngredient, self)._repr_fmt().format("{},{},{}".format(self.amount, self.unit, self.specifier))

    def convert(self, new_unit, rounded=True):
        if self.unit == 'literal':
            return
        self.amount = self.get_amount_as(new_unit, rounded=rounded)
        self.unit = new_unit

    def get_amount_as(self, new_unit, rounded=True, single_value=False):
        if self.unit == 'literal':
            return
        if isinstance(self.amount, tuple):
            lower  = util.convert_units(self.amount[0], self.unit, new_unit, rounded=rounded)
            higher = util.convert_units(self.amount[1], self.unit, new_unit, rounded=rounded)
            amount = (lower+higher)/2.0 if single_value else (lower, higher)
        else:
            amount = util.convert_units(self.amount, self.unit, new_unit, rounded=rounded)
        return amount

    def str(self):
        if self.top_with:
            return "Top with {}".format(self.specifier)
        if self.unit == 'literal':
            return "{} {}".format(self.amount, self.specifier)

        if self.unit == 'ds':
            if isinstance(self.amount, tuple):
                amount = "{:.0f} to {:.0f}".format(self.amount[0], self.amount[1])
                unit = 'dashes'
            elif self.amount == 1:
                amount = 'dash'
                unit = 'of'
            else:
                amount = "{:.0f}".format(self.amount)
                unit = 'dashes'
        elif self.unit == 'drop':
            if isinstance(self.amount, tuple):
                amount = "{:.0f} to {:.0f}".format(self.amount[0], self.amount[1])
                unit = 'drops'
            elif self.amount == 1:
                amount = 'drop'
                unit = 'of'
            else:
                amount = "{:.0f}".format(self.amount)
                unit = 'drops'
        elif self.unit in ['oz', 'tsp']:
            amount = util.to_fraction(self.amount)
            unit = self.unit
        elif self.unit == 'mL':
            unit = self.unit
            if self.amount < 10.0:
                amount = "{:.1f}".format(self.amount)
            else:
                amount = "{:.0f}".format(self.amount)
        else:
            amount = self.amount
            unit = self.unit

        formats = {
                #'mL': u"{:.0f} {} {}",
                'cL': u"{:.1f} {} {}",
                }
        return formats.get(self.unit, u"{} {} {}").format(amount, unit, self.specifier)

    def get_cost(self, bottle, barstock):
        if self.unit == 'literal':
            return 0
        amount = self.get_amount_as(self.recipe_unit, rounded=False, single_value=True)
        return barstock.cost_by_bottle_and_volume(util.IngredientSpecifier(self.specifier.what, bottle), amount, self.recipe_unit)

    def get_std_drinks(self, bottle, barstock):
        if self.unit == 'literal':
            return 0
        amount = self.get_amount_as(self.recipe_unit, rounded=False, single_value=True)
        proof = barstock.get_bottle_proof(util.IngredientSpecifier(self.specifier.what, bottle))
        return util.calculate_std_drinks(proof, amount, self.recipe_unit)

class OptionalIngredient(QuantizedIngredient):
    """ A quantized ingredient that just gets an extra output tag
    """
    def str(self):
        return "{}, (optional)".format(super(OptionalIngredient, self).str())

<|endoftext|>


<commit_before>import core
import tappedout
import praw
import logging
import time
import traceback
import datetime

TESTING = False

PRAW = praw.Reddit(user_agent=core.USER_AGENT)
PRAW.login(*open('login.txt').read().strip().split())

BOT_NOTICE = """
\n\nI'm a bot - visit me in /r/edhrec or [edhrec.com](http://edhrec.com)"""

# The universal easy sleep command
def sleep(t=5.0, x=1.0):
    time.sleep(t * x)

# Given a submission, try to find the tappedout.net URL.
# If there is one, return it. Otherwise, return None.
def find_tappedout_url(submission):
    op_text = submission.selftext.lower().replace('\n', ' ').strip()

    url = tappedout.URL_PATTERN.match(op_text)

    if url is None:
        url = tappedout.URL_PATTERN.match(submission.url)

    if url is None:
        return None
    else:
        return str(url.group(1))

def linkify(cn):
    return '[%s](http://gatherer.wizards.com/Handlers/Image.ashx?name=%s&type=card&.jpg)' % (core.cap_cardname(cn), cn)

# Go through recent submissions and try to find something I haven't seen before.
# If there is something, post the recommendations. This is the default behavior
#    that edhrec does to respond to posts.
def seek_submissions(sublimit=200):
    logging.debug('STARTING SUBMISSION SEEK AT ' + str(datetime.datetime.now()))

    # Scan edh and edhrec
    subreddit = PRAW.get_subreddit('edhrec+edh').get_new(limit=sublimit)

    rds = core.get_redis()

    # For each submission in newness order...
    for submission in subreddit:
        # Check to see if I've scanned this already. If so, pass on it.
        if not TESTING:
            if rds.sismember('SEEN', submission.id):
                continue
        logging.debug("Scanning " + str(submission.id) + " - " + str(submission.title.encode('utf-8')))

        # Fetch the tappedout url
        url = find_tappedout_url(submission)

        # If there was no tappedout URL, then let's pass over this one.
        if url is None:
            rds.sadd('SEEN', submission.id)
            continue

        ## At this point, we have a deck we'e never seen before that has been posted!
        #
        #       ~*~ GET EXCITED ~*~

        logging.debug("I found a URL to scrape: " + str(url))

        # Scrape it
        deck = tappedout.get_deck(url)

        if deck is None:
            logging.warning('Skipping this URL because something went wrong. (' + submission.title.encode('utf-8') +')')
            rds.sadd('SEEN', submission.id)
            continue

        # Go get the recommendations
        newrecs, outrecs = core.recommend(deck)

        lands = []
        creatures =[]
        noncreatures = []

        for card, score in newrecs:
            # filter out basic lands from being recommendations
            if card in ['swamp', 'island', 'plains', 'mountain', 'forest']:
                continue # there is an annoying thing that happens when people use snow-covered basics
                     # where edhrec will post basic lands as a recommendation. this prevents that

            if score < .3:
                continue

            score = int(score * 100) # make score easier to read

            try:
                types = core.lookup_card(card)['types']
            except:
                logging.warn('something went wong with the card %s, ignoring it' % card)
                continue

            if 'Creature' in types:
                creatures.append((card, score))
            elif 'Land' in types:
                lands.append((card, score))
            else:
                noncreatures.append((card, score))

        # build the output string
        if str(submission.subreddit).lower() in ['edhrec', 'edh']:
            out_str = ['Other decks like yours use:\n\nCreatures | Non-creatures | Lands | Unique in your deck\n:--------|:---------|:---------|:--------']

            for i in range(16):
                try:
                    c = '[%d] %s ' % (creatures[i][1], linkify(creatures[i][0]))
                except IndexError:
                    c = ' '

                try:
                    n = '[%d] %s ' % (noncreatures[i][1], linkify(noncreatures[i][0]))
                except IndexError:
                    n = ' '

                try:
                    l = '[%d] %s ' % (lands[i][1], linkify(lands[i][0]))
                except IndexError:
                    l = ' '

                try:
                    u = '%s ' % linkify(outrecs[i][0])
                except IndexError:
                    u = ' '

                if len(c + n + l) == 3:
                    break

                out_str.append('%s | %s | %s | %s' % (c, n , l, u))

            out_str.append('\n\n[This deck on edhrec.com](http://edhrec.com/#/recommendations?q=' + url + ')')

            out_str.append(BOT_NOTICE)

        elif str(submission.subreddit).lower() == 'edh':
            pass

        elif str(submission.subreddit).lower() == 'competetiveedh':
            pass

        # Post the comment!
        if not TESTING:
            submission.add_comment('\n'.join(out_str))

        logging.debug('comment i think I posted:\n' + '\n'.join(out_str))

        logging.debug("I posted a comment with recommendations!")

        deck['ref'] = 'reddit bot'
        deck['url'] = url
        deck['scrapedate'] = str(datetime.datetime.now())

        core.add_recent(url, core.cap_cardname(deck['commander']), submission.url)

        # Keep track of the fact that I've now processed this deck.
        # It is important that this is last in case the scraping fails and
        #   the problem is later fixed.
        if not TESTING:
            rds.sadd('SEEN', submission.id)
            core.add_deck(deck)

        sleep()

    logging.debug('DONE WITH SUBMISSION SEEK AT ' + str(datetime.datetime.now()))

if __name__ == '__main__':
    while True:
        try:
            # Go find submissions
            seek_submissions()
        except Exception as e:
            logging.warning("Got an unhandled exception! " + str(e))
            logging.warning("traceback:" + str(traceback.format_exc()))

        core.flush_cache()

        # Sleep for a minute. A minute is because your requests/minute seem
        #   to reset every minute for Reddit.
        # edh and edhrec subreddits don't have that much traffic, so no big deal
        #   if someone has to wait at most a minute to get a response.
        sleep(t=30.0)
<commit_msg>Changed it so you type in your username and password for reddit so that i dont store the username password in a file<commit_after>import core
import tappedout
import praw
import logging
import time
import traceback
import datetime
import getpass

TESTING = False

PRAW = praw.Reddit(user_agent=core.USER_AGENT)
PRAW.login(raw_input('user name: ').strip(), getpass.getpass().strip())

BOT_NOTICE = """
\n\nI'm a bot - visit me in /r/edhrec or [edhrec.com](http://edhrec.com)"""

logging.basicConfig(filename='reddit.log')

# The universal easy sleep command
def sleep(t=5.0, x=1.0):
    time.sleep(t * x)

# Given a submission, try to find the tappedout.net URL.
# If there is one, return it. Otherwise, return None.
def find_tappedout_url(submission):
    op_text = submission.selftext.lower().replace('\n', ' ').strip()

    url = tappedout.URL_PATTERN.match(op_text)

    if url is None:
        url = tappedout.URL_PATTERN.match(submission.url)

    if url is None:
        return None
    else:
        return str(url.group(1))

def linkify(cn):
    return '[%s](http://gatherer.wizards.com/Handlers/Image.ashx?name=%s&type=card&.jpg)' % (core.cap_cardname(cn), cn)

# Go through recent submissions and try to find something I haven't seen before.
# If there is something, post the recommendations. This is the default behavior
#    that edhrec does to respond to posts.
def seek_submissions(sublimit=200):
    logging.debug('STARTING SUBMISSION SEEK AT ' + str(datetime.datetime.now()))

    # Scan edh and edhrec
    subreddit = PRAW.get_subreddit('edhrec+edh').get_new(limit=sublimit)

    rds = core.get_redis()

    # For each submission in newness order...
    for submission in subreddit:
        # Check to see if I've scanned this already. If so, pass on it.
        if not TESTING:
            if rds.sismember('SEEN', submission.id):
                continue
        logging.debug("Scanning " + str(submission.id) + " - " + str(submission.title.encode('utf-8')))

        # Fetch the tappedout url
        url = find_tappedout_url(submission)

        # If there was no tappedout URL, then let's pass over this one.
        if url is None:
            rds.sadd('SEEN', submission.id)
            continue

        ## At this point, we have a deck we'e never seen before that has been posted!
        #
        #       ~*~ GET EXCITED ~*~

        logging.debug("I found a URL to scrape: " + str(url))

        # Scrape it
        deck = tappedout.get_deck(url)

        if deck is None:
            logging.warning('Skipping this URL because something went wrong. (' + submission.title.encode('utf-8') +')')
            rds.sadd('SEEN', submission.id)
            continue

        # Go get the recommendations
        newrecs, outrecs = core.recommend(deck)

        lands = []
        creatures =[]
        noncreatures = []

        for card, score in newrecs:
            # filter out basic lands from being recommendations
            if card in ['swamp', 'island', 'plains', 'mountain', 'forest']:
                continue # there is an annoying thing that happens when people use snow-covered basics
                     # where edhrec will post basic lands as a recommendation. this prevents that

            if score < .3:
                continue

            score = int(score * 100) # make score easier to read

            try:
                types = core.lookup_card(card)['types']
            except:
                logging.warn('something went wong with the card %s, ignoring it' % card)
                continue

            if 'Creature' in types:
                creatures.append((card, score))
            elif 'Land' in types:
                lands.append((card, score))
            else:
                noncreatures.append((card, score))

        # build the output string
        if str(submission.subreddit).lower() in ['edhrec', 'edh']:
            out_str = ['Other decks like yours use:\n\nCreatures | Non-creatures | Lands | Unique in your deck\n:--------|:---------|:---------|:--------']

            for i in range(16):
                try:
                    c = '[%d] %s ' % (creatures[i][1], linkify(creatures[i][0]))
                except IndexError:
                    c = ' '

                try:
                    n = '[%d] %s ' % (noncreatures[i][1], linkify(noncreatures[i][0]))
                except IndexError:
                    n = ' '

                try:
                    l = '[%d] %s ' % (lands[i][1], linkify(lands[i][0]))
                except IndexError:
                    l = ' '

                try:
                    u = '%s ' % linkify(outrecs[i][0])
                except IndexError:
                    u = ' '

                if len(c + n + l) == 3:
                    break

                out_str.append('%s | %s | %s | %s' % (c, n , l, u))

            out_str.append('\n\n[This deck on edhrec.com](http://edhrec.com/#/recommendations?q=' + url + ')')

            out_str.append(BOT_NOTICE)

        elif str(submission.subreddit).lower() == 'edh':
            pass

        elif str(submission.subreddit).lower() == 'competetiveedh':
            pass

        # Post the comment!
        if not TESTING:
            submission.add_comment('\n'.join(out_str))

        logging.debug('comment i think I posted:\n' + '\n'.join(out_str))

        logging.debug("I posted a comment with recommendations!")

        deck['ref'] = 'reddit bot'
        deck['url'] = url
        deck['scrapedate'] = str(datetime.datetime.now())

        core.add_recent(url, core.cap_cardname(deck['commander']), submission.url)

        # Keep track of the fact that I've now processed this deck.
        # It is important that this is last in case the scraping fails and
        #   the problem is later fixed.
        if not TESTING:
            rds.sadd('SEEN', submission.id)
            core.add_deck(deck)

        sleep()

    logging.debug('DONE WITH SUBMISSION SEEK AT ' + str(datetime.datetime.now()))

if __name__ == '__main__':
    while True:
        try:
            # Go find submissions
            seek_submissions()
        except Exception as e:
            logging.warning("Got an unhandled exception! " + str(e))
            logging.warning("traceback:" + str(traceback.format_exc()))

        core.flush_cache()

        # Sleep for a minute. A minute is because your requests/minute seem
        #   to reset every minute for Reddit.
        # edh and edhrec subreddits don't have that much traffic, so no big deal
        #   if someone has to wait at most a minute to get a response.
        sleep(t=30.0)
<|endoftext|>


<commit_before>from itertools import chain, ifilter, imap
from py2neo import neo4j
from metadata import metadata as m
from relationship import Relationship
from node import Node
from util import IN, OUT

class RelList(list):

    def __init__(self, direction, *args, **kwargs):
        super(RelList, self).__init__(*args, **kwargs)
        self.direction = direction
        self.nodes = {}
        for rel in self:
            node = self._nodefunc(rel)
            if node not in self.nodes:
                self.nodes[node] = set([rel])
            else:
                self.nodes[node].add(rel)

    def _nodefunc(self, rel):
        return rel.end if self.direction == OUT else rel.start

    def rel(self, node):
        return self.nodes.get(node, set())

    def append(self, rel):
        super(RelList, self).append(rel)
        node = self._nodefunc(rel)
        if node not in self.nodes:
            self.nodes[node] = set([rel])
        else:
            self.nodes[node].add(rel)

    def remove(self, rel):
        try:
            super(RelList, self).remove(rel)
        except ValueError:
            pass
        else:
            node = self._nodefunc(rel)
            if node in self.nodes:
                self.nodes[node].discard(rel)
                if len(self.nodes[node]) == 0:
                    del self.nodes[node]

class RelMap(object):

    def __init__(self):
        self._ids = {}
        self._phantoms = set()
        self.node = {}
        self.start = {}
        self.end = {}

    def clear(self):
        self._ids.clear()
        self._phantoms.clear()
        self.node.clear()
        self.start.clear()
        self.end.clear()

    def _track(self, rel):
        if rel.id is not None:
            self._phantoms.discard(rel)
            self._ids[rel.id] = rel
        else:
            self._phantoms.add(rel)
        rel._relmap = self

    def _untrack(self, rel):
        self._ids.pop(rel.id, None)
        self._phantoms.discard(rel)
        rel._relmap = None

    def _map(self, rel):
        if not self.node.has_key(rel.start):
            self.node[rel.start] = set()
        self.node[rel.start].add(rel)

        if not self.node.has_key(rel.end):
            self.node[rel.end] = set()
        self.node[rel.end].add(rel)

        if not self.start.has_key((rel.start, rel.type)):
            self.start[(rel.start, rel.type)] = RelList(OUT)
        self.start[(rel.start, rel.type)].append(rel)

        if not self.end.has_key((rel.end, rel.type)):
            self.end[(rel.end, rel.type)] = RelList(IN)
        self.end[(rel.end, rel.type)].append(rel)

    def _unmap(self, rel):
        try:
            self.node[rel.start].discard(rel)
            self.node[rel.end].discard(rel)
            self.start[(rel.start, rel.type)].remove(rel)
            self.end[(rel.end, rel.type)].remove(rel)
        except KeyError:
            pass

    def add(self, rel):
        self._track(rel)
        self.update(rel)

    def update(self, rel):
        if not rel.is_deleted():
            self._map(rel)
        else:
            self._unmap(rel)

    def remove(self, rel):
        self._untrack(rel)
        self._unmap(rel)

    def get(self, value):
        if isinstance(value, int):
            return self._ids.get(value)
        elif isinstance(value, neo4j.Relationship):
            return self._ids.get(value.id)
        else:
            return None

    def __len__(self):
        return len(self._ids) + len(self._phantoms)

    def __iter__(self):
        return chain(self._ids.itervalues(), iter(self._phantoms))

    def iterphantoms(self):
        return iter(self._phantoms)

    def iterpersisted(self):
        return self._ids.itervalues()

    def rollback(self):
        try:
            while True:
                rel = self._phantoms.pop()
                self._unmap(rel)
        except KeyError:
            pass
        for rel in self._ids.itervalues():
            rel.rollback()

    def load_rels(self, node, direction, type):
        if not node.is_phantom():
            q = 'start n=node({node_id})'
            if direction is OUT:
                q += ' match n-[r:{0}]->o return r, o'.format(type)
            else:
                q += ' match n<-[r:{0}]-o return r, o'.format(type)

            m.cypher(q, params={'node_id': node.id})

class RelView(object):

    __default_cls__ = Relationship

    def __init__(self, owner, direction, type_, multiple=False, preloaded=False):
        self.relmap = m.session.relmap
        self.owner = owner
        self.direction = direction
        self.type = getattr(type_, '__rel_type__', type_)
        self.cls = type_ if isinstance(type_, type) else self.__default_cls__
        self.multiple = multiple
        self.preloaded = preloaded
        self._noload = 0

    def load(self):
        self.relmap.load_rels(self.owner, self.direction, self.type)

    @property
    def data(self):
        try:
            return self._data
        except AttributeError:
            if not self.preloaded:
                self.load()
            if self.direction == OUT:
                if not self.relmap.start.has_key((self.owner, self.type)):
                    self.relmap.start[(self.owner, self.type)] = RelList(OUT)
                self._data = self.relmap.start[(self.owner, self.type)]
            else:
                if not self.relmap.end.has_key((self.owner, self.type)):
                    self.relmap.end[(self.owner, self.type)] = RelList(IN)
                self._data = self.relmap.end[(self.owner, self.type)]
            return self._data

    # using 'with' will prevent preloading within its context
    def __enter__(self):
        if not hasattr(self, '_data') and self.preloaded is False or self._noload > 0:
            self._noload += 1
            self.preloaded = True

    def __exit__(self, exc_type, exc_value, traceback):
        if self._noload == 1:
            self._noload = 0
            self.preloaded = False
            if hasattr(self, '_data'):
                delattr(self, '_data')
        elif self._noload > 1:
            self._noload -= 1

    def _nodefunc(self, value):
        if isinstance(value, Relationship):
            return value.end if self.direction == OUT else value.start
        elif isinstance(value, Node):
            return value
        else:
            raise TypeError("unexpected type: " + value.__class__.__name__)

    def _relfunc(self, value):
        if isinstance(value, Relationship):
            return value
        else:
            other = Node(value)
            if other is not None:
                if self.direction == OUT:
                    return self.cls((self.owner, self.type, other))
                else:
                    return self.cls((other, self.type, self.owner))
            else:
                raise ValueError("could not find other Node")

    def __iter__(self):
        return imap(self._nodefunc, self.data)

    def __contains__(self, item):
        if isinstance(item, Relationship):
            return item in self.data
        else:
            return item in iter(self)

    def __len__(self):
        return len(self.data)

    def __repr__(self):
        return "[{0}]".format(", ".join(imap(repr, iter(self))))

    def __getitem__(self, key):
        return self._nodefunc(self.data[key])

    def __delitem__(self, key):
        self.data[key].delete()

    def sorted(self):
        return map(self._nodefunc, sorted(self.data,
                                          cmp=self.cls.__sort_cmp__,
                                          key=self.cls.__sort_key__))

    def reversed(self):
        return map(self._nodefunc, sorted(self.data,
                                          cmp=self.cls.__sort_cmp__,
                                          key=self.cls.__sort_key__,
                                          reverse=True))

    def iterrels(self):
        return iter(self.data)

    def rels(self):
        return self.data

    def node(self, rel):
        return self._nodefunc(rel)

    def rel(self, node):
        rels = list(self.data.rel(node))
        if self.multiple:
            return rels
        else:
            return rels[0]

    def append(self, value):
        if self.multiple or value not in self:
            return self._relfunc(value)
        return None

    def remove(self, value):
        if isinstance(value, Node):
            for rel in list(self.data.rel(value)):
                rel.delete()
        elif isinstance(value, Relationship) and value in self:
            value.delete()
<commit_msg>it's not really private...<commit_after>from itertools import chain, ifilter, imap
from py2neo import neo4j
from metadata import metadata as m
from relationship import Relationship
from node import Node
from util import IN, OUT

class RelList(list):

    def __init__(self, direction, *args, **kwargs):
        super(RelList, self).__init__(*args, **kwargs)
        self.direction = direction
        self.nodes = {}
        for rel in self:
            node = self.nodefunc(rel)
            if node not in self.nodes:
                self.nodes[node] = set([rel])
            else:
                self.nodes[node].add(rel)

    def nodefunc(self, rel):
        return rel.end if self.direction == OUT else rel.start

    def rel(self, node):
        return self.nodes.get(node, set())

    def append(self, rel):
        super(RelList, self).append(rel)
        node = self.nodefunc(rel)
        if node not in self.nodes:
            self.nodes[node] = set([rel])
        else:
            self.nodes[node].add(rel)

    def remove(self, rel):
        try:
            super(RelList, self).remove(rel)
        except ValueError:
            pass
        else:
            node = self.nodefunc(rel)
            if node in self.nodes:
                self.nodes[node].discard(rel)
                if len(self.nodes[node]) == 0:
                    del self.nodes[node]

class RelMap(object):

    def __init__(self):
        self._ids = {}
        self._phantoms = set()
        self.node = {}
        self.start = {}
        self.end = {}

    def clear(self):
        self._ids.clear()
        self._phantoms.clear()
        self.node.clear()
        self.start.clear()
        self.end.clear()

    def _track(self, rel):
        if rel.id is not None:
            self._phantoms.discard(rel)
            self._ids[rel.id] = rel
        else:
            self._phantoms.add(rel)
        rel._relmap = self

    def _untrack(self, rel):
        self._ids.pop(rel.id, None)
        self._phantoms.discard(rel)
        rel._relmap = None

    def _map(self, rel):
        if not self.node.has_key(rel.start):
            self.node[rel.start] = set()
        self.node[rel.start].add(rel)

        if not self.node.has_key(rel.end):
            self.node[rel.end] = set()
        self.node[rel.end].add(rel)

        if not self.start.has_key((rel.start, rel.type)):
            self.start[(rel.start, rel.type)] = RelList(OUT)
        self.start[(rel.start, rel.type)].append(rel)

        if not self.end.has_key((rel.end, rel.type)):
            self.end[(rel.end, rel.type)] = RelList(IN)
        self.end[(rel.end, rel.type)].append(rel)

    def _unmap(self, rel):
        try:
            self.node[rel.start].discard(rel)
            self.node[rel.end].discard(rel)
            self.start[(rel.start, rel.type)].remove(rel)
            self.end[(rel.end, rel.type)].remove(rel)
        except KeyError:
            pass

    def add(self, rel):
        self._track(rel)
        self.update(rel)

    def update(self, rel):
        if not rel.is_deleted():
            self._map(rel)
        else:
            self._unmap(rel)

    def remove(self, rel):
        self._untrack(rel)
        self._unmap(rel)

    def get(self, value):
        if isinstance(value, int):
            return self._ids.get(value)
        elif isinstance(value, neo4j.Relationship):
            return self._ids.get(value.id)
        else:
            return None

    def __len__(self):
        return len(self._ids) + len(self._phantoms)

    def __iter__(self):
        return chain(self._ids.itervalues(), iter(self._phantoms))

    def iterphantoms(self):
        return iter(self._phantoms)

    def iterpersisted(self):
        return self._ids.itervalues()

    def rollback(self):
        try:
            while True:
                rel = self._phantoms.pop()
                self._unmap(rel)
        except KeyError:
            pass
        for rel in self._ids.itervalues():
            rel.rollback()

    def load_rels(self, node, direction, type):
        if not node.is_phantom():
            q = 'start n=node({node_id})'
            if direction is OUT:
                q += ' match n-[r:{0}]->o return r, o'.format(type)
            else:
                q += ' match n<-[r:{0}]-o return r, o'.format(type)

            m.cypher(q, params={'node_id': node.id})

class RelView(object):

    __default_cls__ = Relationship

    def __init__(self, owner, direction, type_, multiple=False, preloaded=False):
        self.relmap = m.session.relmap
        self.owner = owner
        self.direction = direction
        self.type = getattr(type_, '__rel_type__', type_)
        self.cls = type_ if isinstance(type_, type) else self.__default_cls__
        self.multiple = multiple
        self.preloaded = preloaded
        self._noload = 0

    def load(self):
        self.relmap.load_rels(self.owner, self.direction, self.type)

    @property
    def data(self):
        try:
            return self._data
        except AttributeError:
            if not self.preloaded:
                self.load()
            if self.direction == OUT:
                if not self.relmap.start.has_key((self.owner, self.type)):
                    self.relmap.start[(self.owner, self.type)] = RelList(OUT)
                self._data = self.relmap.start[(self.owner, self.type)]
            else:
                if not self.relmap.end.has_key((self.owner, self.type)):
                    self.relmap.end[(self.owner, self.type)] = RelList(IN)
                self._data = self.relmap.end[(self.owner, self.type)]
            return self._data

    # using 'with' will prevent preloading within its context
    def __enter__(self):
        if not hasattr(self, '_data') and self.preloaded is False or self._noload > 0:
            self._noload += 1
            self.preloaded = True

    def __exit__(self, exc_type, exc_value, traceback):
        if self._noload == 1:
            self._noload = 0
            self.preloaded = False
            if hasattr(self, '_data'):
                delattr(self, '_data')
        elif self._noload > 1:
            self._noload -= 1

    def nodefunc(self, value):
        if isinstance(value, Relationship):
            return value.end if self.direction == OUT else value.start
        elif isinstance(value, Node):
            return value
        else:
            raise TypeError("unexpected type: " + value.__class__.__name__)

    def relfunc(self, value):
        if isinstance(value, Relationship):
            return value
        else:
            other = Node(value)
            if other is not None:
                if self.direction == OUT:
                    return self.cls((self.owner, self.type, other))
                else:
                    return self.cls((other, self.type, self.owner))
            else:
                raise ValueError("could not find other Node")

    def __iter__(self):
        return imap(self.nodefunc, self.data)

    def __contains__(self, item):
        if isinstance(item, Relationship):
            return item in self.data
        else:
            return item in iter(self)

    def __len__(self):
        return len(self.data)

    def __repr__(self):
        return "[{0}]".format(", ".join(imap(repr, iter(self))))

    def __getitem__(self, key):
        return self.nodefunc(self.data[key])

    def __delitem__(self, key):
        self.data[key].delete()

    def sorted(self):
        return map(self.nodefunc, sorted(self.data,
                                          cmp=self.cls.__sort_cmp__,
                                          key=self.cls.__sort_key__))

    def reversed(self):
        return map(self.nodefunc, sorted(self.data,
                                          cmp=self.cls.__sort_cmp__,
                                          key=self.cls.__sort_key__,
                                          reverse=True))

    def iterrels(self):
        return iter(self.data)

    def rels(self):
        return self.data

    def node(self, rel):
        return self.nodefunc(rel)

    def rel(self, node):
        rels = list(self.data.rel(node))
        if self.multiple:
            return rels
        else:
            return rels[0]

    def append(self, value):
        if self.multiple or value not in self:
            return self.relfunc(value)
        return None

    def remove(self, value):
        if isinstance(value, Node):
            for rel in list(self.data.rel(value)):
                rel.delete()
        elif isinstance(value, Relationship) and value in self:
            value.delete()
<|endoftext|>