<commit_before># -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import OrderedDict

import json
import datetime

from odoo import api, fields, models, _
from odoo.exceptions import AccessError, ValidationError
from odoo.addons import decimal_precision as dp

class LunchOrder(models.Model):
    """
    A lunch order contains one or more lunch order line(s). It is associated to a user for a given
    date. When creating a lunch order, applicable lunch alerts are displayed.
    """
    _name = 'lunch.order'
    _description = 'Lunch Order'
    _order = 'date desc'

    def _default_previous_order_ids(self):
        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc')
        # If we return return prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        return list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

    user_id = fields.Many2one('res.users', 'User', readonly=True,
                              states={'new': [('readonly', False)]},
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, readonly=True,
                       states={'new': [('readonly', False)]},
                       default=fields.Date.context_today)
    order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products',
                                     readonly=True, copy=True,
                                     states={'new': [('readonly', False)], False: [('readonly', False)]})
    total = fields.Float(compute='_compute_total', string="Total", store=True)
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, copy=False,
                             compute='_compute_order_state', store=True)
    alerts = fields.Text(compute='_compute_alerts_get', string="Alerts")
    company_id = fields.Many2one('res.company', related='user_id.company_id', store=True)
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True)
    cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance')
    balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance')
    previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order')
    previous_order_widget = fields.Text(compute='_compute_previous_order')

    @api.one
    @api.depends('order_line_ids')
    def _compute_total(self):
        """
        get and sum the order lines' price
        """
        self.total = sum(
            orderline.price for orderline in self.order_line_ids)

    @api.multi
    def name_get(self):
        return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self]

    @api.depends('state')
    def _compute_alerts_get(self):
        """
        get the alerts to display on the order form
        """
        alert_msg = [alert.message
                     for alert in self.env['lunch.alert'].search([])
                     if alert.display]

        if self.state == 'new':
            self.alerts = alert_msg and '\n'.join(alert_msg) or False

    @api.multi
    @api.depends('user_id', 'state')
    def _compute_previous_order(self):
        self.ensure_one()
        self.previous_order_widget = json.dumps(False)

        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc')
        # If we use prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        self.previous_order_ids = list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

        if self.previous_order_ids:
            lunch_data = {}
            for line in self.previous_order_ids:
                lunch_data[line.id] = {
                    'line_id': line.id,
                    'product_id': line.product_id.id,
                    'product_name': line.product_id.name,
                    'supplier': line.supplier.name,
                    'note': line.note,
                    'price': line.price,
                    'date': fields.Date.to_string(line.date),
                    'currency_id': line.currency_id.id,
                }
            # sort the old lunch orders by (date, id)
            lunch_data = OrderedDict(sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True))
            self.previous_order_widget = json.dumps(lunch_data)

    @api.one
    @api.depends('user_id')
    def _compute_cash_move_balance(self):
        domain = [('user_id', '=', self.user_id.id)]
        lunch_cash = self.env['lunch.cashmove'].read_group(domain, ['amount', 'user_id'], ['user_id'])
        if len(lunch_cash):
            self.cash_move_balance = lunch_cash[0]['amount']
        self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups('lunch.group_lunch_manager')

    @api.one
    @api.constrains('date')
    def _check_date(self):
        """
        Prevents the user to create an order in the past
        """
        date_order = self.date
        date_today = fields.Date.context_today(self)
        if date_order < date_today:
            raise ValidationError(_('The date of your order is in the past.'))

    @api.one
    @api.depends('order_line_ids.state')
    def _compute_order_state(self):
        """
        Update the state of lunch.order based on its orderlines. Here is the logic:
        - if at least one order line is cancelled, the order is set as cancelled
        - if no line is cancelled but at least one line is not confirmed, the order is set as new
        - if all lines are confirmed, the order is set as confirmed
        """
        if not self.order_line_ids:
            self.state = 'new'
        else:
            isConfirmed = True
            for orderline in self.order_line_ids:
                if orderline.state == 'cancelled':
                    self.state = 'cancelled'
                    return
                elif orderline.state == 'confirmed':
                    continue
                else:
                    isConfirmed = False

            if isConfirmed:
                self.state = 'confirmed'
            else:
                self.state = 'new'
        return

class LunchOrderLine(models.Model):
    _name = 'lunch.order.line'
    _description = 'lunch order line'
    _order = 'date desc, id desc'

    name = fields.Char(related='product_id.name', string="Product Name", readonly=True)
    order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True)
    product_id = fields.Many2one('lunch.product', 'Product', required=True)
    category_id = fields.Many2one('lunch.product.category', string='Product Category',
                                  related='product_id.category_id', readonly=True, store=True)
    date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True)
    supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier',
                               readonly=True, store=True)
    user_id = fields.Many2one('res.users', string='User', related='order_id.user_id',
                              readonly=True, store=True)
    note = fields.Text('Note')
    price = fields.Float(related='product_id.price', readonly=True, store=True,
                         digits=dp.get_precision('Account'))
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('ordered', 'Ordered'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, default='new')
    cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move')
    currency_id = fields.Many2one('res.currency', related='order_id.currency_id')

    @api.one
    def order(self):
        """
        The order_line is ordered to the vendor but isn't received yet
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'ordered'
        else:
            raise AccessError(_("Only your lunch manager processes the orders."))

    @api.one
    def confirm(self):
        """
        confirm one or more order line, update order status and create new cashmove
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            if self.state != 'confirmed':
                values = {
                    'user_id': self.user_id.id,
                    'amount': -self.price,
                    'description': self.product_id.name,
                    'order_id': self.id,
                    'state': 'order',
                    'date': self.date,
                }
                self.env['lunch.cashmove'].create(values)
                self.state = 'confirmed'
        else:
            raise AccessError(_("Only your lunch manager sets the orders as received."))

    @api.one
    def cancel(self):
        """
        cancel one or more order.line, update order status and unlink existing cashmoves
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'cancelled'
            self.cashmove.unlink()
        else:
            raise AccessError(_("Only your lunch manager cancels the orders."))

class LunchProduct(models.Model):
    """ Products available to order. A product is linked to a specific vendor. """
    _name = 'lunch.product'
    _description = 'lunch product'

    name = fields.Char('Product', required=True)
    category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True)
    description = fields.Text('Description')
    price = fields.Float('Price', digits=dp.get_precision('Account'))
    supplier = fields.Many2one('res.partner', 'Vendor')
    active = fields.Boolean(default=True)

class LunchProductCategory(models.Model):
    """ Category of the product such as pizza, sandwich, pasta, chinese, burger... """
    _name = 'lunch.product.category'
    _description = 'lunch product category'

    name = fields.Char('Product Category', required=True)

class LunchCashMove(models.Model):
    """ Two types of cashmoves: payment (credit) or order (debit) """
    _name = 'lunch.cashmove'
    _description = 'lunch cashmove'

    user_id = fields.Many2one('res.users', 'User',
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, default=fields.Date.context_today)
    amount = fields.Float('Amount', required=True, help='Can be positive (payment) or negative (order or payment if user wants to get his money back)')
    description = fields.Text('Description', help='Can be an order or a payment')
    order_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade')
    state = fields.Selection([('order', 'Order'), ('payment', 'Payment')],
                             'Is an order or a payment', default='payment')

    @api.multi
    def name_get(self):
        return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]

class LunchAlert(models.Model):
    """ Alerts to display during a lunch order. An alert can be specific to a
    given day, weekly or daily. The alert is displayed from start to end hour. """
    _name = 'lunch.alert'
    _description = 'Lunch Alert'

    display = fields.Boolean(compute='_compute_display_get')
    message = fields.Text('Message', required=True)
    alert_type = fields.Selection([('specific', 'Specific Day'),
                                   ('week', 'Every Week'),
                                   ('days', 'Every Day')],
                                  string='Recurrence', required=True, index=True, default='specific')
    specific_day = fields.Date('Day', default=fields.Date.context_today)
    monday = fields.Boolean('Monday')
    tuesday = fields.Boolean('Tuesday')
    wednesday = fields.Boolean('Wednesday')
    thursday = fields.Boolean('Thursday')
    friday = fields.Boolean('Friday')
    saturday = fields.Boolean('Saturday')
    sunday = fields.Boolean('Sunday')
    start_hour = fields.Float('Between', oldname='active_from', required=True, default=7)
    end_hour = fields.Float('And', oldname='active_to', required=True, default=23)
    active = fields.Boolean(default=True)

    @api.multi
    def name_get(self):
        return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self]

    @api.one
    def _compute_display_get(self):
        """
        This method check if the alert can be displayed today
        if alert type is specific : compare specific_day(date) with today's date
        if alert type is week : check today is set as alert (checkbox true) eg. self['monday']
        if alert type is day : True
        return : Message if can_display_alert is True else False
        """

        days_codes = {'0': 'sunday',
                      '1': 'monday',
                      '2': 'tuesday',
                      '3': 'wednesday',
                      '4': 'thursday',
                      '5': 'friday',
                      '6': 'saturday'}
        can_display_alert = {
            'specific': (self.specific_day == fields.Date.context_today(self)),
            'week': self[days_codes[datetime.datetime.now().strftime('%w')]],
            'days': True
        }

        if can_display_alert[self.alert_type]:
            mynow = fields.Datetime.context_timestamp(self, datetime.datetime.now())
            hour_to = int(self.end_hour)
            min_to = int((self.end_hour - hour_to) * 60)
            to_alert = datetime.time(hour_to, min_to)
            hour_from = int(self.start_hour)
            min_from = int((self.start_hour - hour_from) * 60)
            from_alert = datetime.time(hour_from, min_from)

            if from_alert <= mynow.time() <= to_alert:
                self.display = True
            else:
                self.display = False
<commit_msg>[FIX] lunch: correctly compare dates<commit_after># -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from collections import OrderedDict

import json
import datetime

from odoo import api, fields, models, _
from odoo.exceptions import AccessError, ValidationError
from odoo.addons import decimal_precision as dp

class LunchOrder(models.Model):
    """
    A lunch order contains one or more lunch order line(s). It is associated to a user for a given
    date. When creating a lunch order, applicable lunch alerts are displayed.
    """
    _name = 'lunch.order'
    _description = 'Lunch Order'
    _order = 'date desc'

    def _default_previous_order_ids(self):
        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc')
        # If we return return prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        return list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

    user_id = fields.Many2one('res.users', 'User', readonly=True,
                              states={'new': [('readonly', False)]},
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, readonly=True,
                       states={'new': [('readonly', False)]},
                       default=fields.Date.context_today)
    order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products',
                                     readonly=True, copy=True,
                                     states={'new': [('readonly', False)], False: [('readonly', False)]})
    total = fields.Float(compute='_compute_total', string="Total", store=True)
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, copy=False,
                             compute='_compute_order_state', store=True)
    alerts = fields.Text(compute='_compute_alerts_get', string="Alerts")
    company_id = fields.Many2one('res.company', related='user_id.company_id', store=True)
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True)
    cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance')
    balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance')
    previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order')
    previous_order_widget = fields.Text(compute='_compute_previous_order')

    @api.one
    @api.depends('order_line_ids')
    def _compute_total(self):
        """
        get and sum the order lines' price
        """
        self.total = sum(
            orderline.price for orderline in self.order_line_ids)

    @api.multi
    def name_get(self):
        return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self]

    @api.depends('state')
    def _compute_alerts_get(self):
        """
        get the alerts to display on the order form
        """
        alert_msg = [alert.message
                     for alert in self.env['lunch.alert'].search([])
                     if alert.display]

        if self.state == 'new':
            self.alerts = alert_msg and '\n'.join(alert_msg) or False

    @api.multi
    @api.depends('user_id', 'state')
    def _compute_previous_order(self):
        self.ensure_one()
        self.previous_order_widget = json.dumps(False)

        prev_order = self.env['lunch.order.line'].search([('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc')
        # If we use prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        self.previous_order_ids = list({
            (order.product_id, order.note): order.id
            for order in prev_order
        }.values())

        if self.previous_order_ids:
            lunch_data = {}
            for line in self.previous_order_ids:
                lunch_data[line.id] = {
                    'line_id': line.id,
                    'product_id': line.product_id.id,
                    'product_name': line.product_id.name,
                    'supplier': line.supplier.name,
                    'note': line.note,
                    'price': line.price,
                    'date': fields.Date.to_string(line.date),
                    'currency_id': line.currency_id.id,
                }
            # sort the old lunch orders by (date, id)
            lunch_data = OrderedDict(sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True))
            self.previous_order_widget = json.dumps(lunch_data)

    @api.one
    @api.depends('user_id')
    def _compute_cash_move_balance(self):
        domain = [('user_id', '=', self.user_id.id)]
        lunch_cash = self.env['lunch.cashmove'].read_group(domain, ['amount', 'user_id'], ['user_id'])
        if len(lunch_cash):
            self.cash_move_balance = lunch_cash[0]['amount']
        self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups('lunch.group_lunch_manager')

    @api.one
    @api.constrains('date')
    def _check_date(self):
        """
        Prevents the user to create an order in the past
        """
        date_order = self.date
        date_today = fields.Date.context_today(self)
        if date_order < date_today:
            raise ValidationError(_('The date of your order is in the past.'))

    @api.one
    @api.depends('order_line_ids.state')
    def _compute_order_state(self):
        """
        Update the state of lunch.order based on its orderlines. Here is the logic:
        - if at least one order line is cancelled, the order is set as cancelled
        - if no line is cancelled but at least one line is not confirmed, the order is set as new
        - if all lines are confirmed, the order is set as confirmed
        """
        if not self.order_line_ids:
            self.state = 'new'
        else:
            isConfirmed = True
            for orderline in self.order_line_ids:
                if orderline.state == 'cancelled':
                    self.state = 'cancelled'
                    return
                elif orderline.state == 'confirmed':
                    continue
                else:
                    isConfirmed = False

            if isConfirmed:
                self.state = 'confirmed'
            else:
                self.state = 'new'
        return

class LunchOrderLine(models.Model):
    _name = 'lunch.order.line'
    _description = 'lunch order line'
    _order = 'date desc, id desc'

    name = fields.Char(related='product_id.name', string="Product Name", readonly=True)
    order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True)
    product_id = fields.Many2one('lunch.product', 'Product', required=True)
    category_id = fields.Many2one('lunch.product.category', string='Product Category',
                                  related='product_id.category_id', readonly=True, store=True)
    date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True)
    supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier',
                               readonly=True, store=True)
    user_id = fields.Many2one('res.users', string='User', related='order_id.user_id',
                              readonly=True, store=True)
    note = fields.Text('Note')
    price = fields.Float(related='product_id.price', readonly=True, store=True,
                         digits=dp.get_precision('Account'))
    state = fields.Selection([('new', 'New'),
                              ('confirmed', 'Received'),
                              ('ordered', 'Ordered'),
                              ('cancelled', 'Cancelled')],
                             'Status', readonly=True, index=True, default='new')
    cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move')
    currency_id = fields.Many2one('res.currency', related='order_id.currency_id')

    @api.one
    def order(self):
        """
        The order_line is ordered to the vendor but isn't received yet
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'ordered'
        else:
            raise AccessError(_("Only your lunch manager processes the orders."))

    @api.one
    def confirm(self):
        """
        confirm one or more order line, update order status and create new cashmove
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            if self.state != 'confirmed':
                values = {
                    'user_id': self.user_id.id,
                    'amount': -self.price,
                    'description': self.product_id.name,
                    'order_id': self.id,
                    'state': 'order',
                    'date': self.date,
                }
                self.env['lunch.cashmove'].create(values)
                self.state = 'confirmed'
        else:
            raise AccessError(_("Only your lunch manager sets the orders as received."))

    @api.one
    def cancel(self):
        """
        cancel one or more order.line, update order status and unlink existing cashmoves
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'cancelled'
            self.cashmove.unlink()
        else:
            raise AccessError(_("Only your lunch manager cancels the orders."))

class LunchProduct(models.Model):
    """ Products available to order. A product is linked to a specific vendor. """
    _name = 'lunch.product'
    _description = 'lunch product'

    name = fields.Char('Product', required=True)
    category_id = fields.Many2one('lunch.product.category', 'Product Category', required=True)
    description = fields.Text('Description')
    price = fields.Float('Price', digits=dp.get_precision('Account'))
    supplier = fields.Many2one('res.partner', 'Vendor')
    active = fields.Boolean(default=True)

class LunchProductCategory(models.Model):
    """ Category of the product such as pizza, sandwich, pasta, chinese, burger... """
    _name = 'lunch.product.category'
    _description = 'lunch product category'

    name = fields.Char('Product Category', required=True)

class LunchCashMove(models.Model):
    """ Two types of cashmoves: payment (credit) or order (debit) """
    _name = 'lunch.cashmove'
    _description = 'lunch cashmove'

    user_id = fields.Many2one('res.users', 'User',
                              default=lambda self: self.env.uid)
    date = fields.Date('Date', required=True, default=fields.Date.context_today)
    amount = fields.Float('Amount', required=True, help='Can be positive (payment) or negative (order or payment if user wants to get his money back)')
    description = fields.Text('Description', help='Can be an order or a payment')
    order_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade')
    state = fields.Selection([('order', 'Order'), ('payment', 'Payment')],
                             'Is an order or a payment', default='payment')

    @api.multi
    def name_get(self):
        return [(cashmove.id, '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id)) for cashmove in self]

class LunchAlert(models.Model):
    """ Alerts to display during a lunch order. An alert can be specific to a
    given day, weekly or daily. The alert is displayed from start to end hour. """
    _name = 'lunch.alert'
    _description = 'Lunch Alert'

    display = fields.Boolean(compute='_compute_display_get')
    message = fields.Text('Message', required=True)
    alert_type = fields.Selection([('specific', 'Specific Day'),
                                   ('week', 'Every Week'),
                                   ('days', 'Every Day')],
                                  string='Recurrence', required=True, index=True, default='specific')
    specific_day = fields.Date('Day', default=fields.Date.context_today)
    monday = fields.Boolean('Monday')
    tuesday = fields.Boolean('Tuesday')
    wednesday = fields.Boolean('Wednesday')
    thursday = fields.Boolean('Thursday')
    friday = fields.Boolean('Friday')
    saturday = fields.Boolean('Saturday')
    sunday = fields.Boolean('Sunday')
    start_hour = fields.Float('Between', oldname='active_from', required=True, default=7)
    end_hour = fields.Float('And', oldname='active_to', required=True, default=23)
    active = fields.Boolean(default=True)

    @api.multi
    def name_get(self):
        return [(alert.id, '%s %s' % (_('Alert'), '#%d' % alert.id)) for alert in self]

    @api.one
    def _compute_display_get(self):
        """
        This method check if the alert can be displayed today
        if alert type is specific : compare specific_day(date) with today's date
        if alert type is week : check today is set as alert (checkbox true) eg. self['monday']
        if alert type is day : True
        return : Message if can_display_alert is True else False
        """

        days_codes = {'0': 'sunday',
                      '1': 'monday',
                      '2': 'tuesday',
                      '3': 'wednesday',
                      '4': 'thursday',
                      '5': 'friday',
                      '6': 'saturday'}
        can_display_alert = {
            'specific': (str(self.specific_day) == fields.Date.context_today(self)),
            'week': self[days_codes[datetime.datetime.now().strftime('%w')]],
            'days': True
        }

        if can_display_alert[self.alert_type]:
            mynow = fields.Datetime.context_timestamp(self, datetime.datetime.now())
            hour_to = int(self.end_hour)
            min_to = int((self.end_hour - hour_to) * 60)
            to_alert = datetime.time(hour_to, min_to)
            hour_from = int(self.start_hour)
            min_from = int((self.start_hour - hour_from) * 60)
            from_alert = datetime.time(hour_from, min_from)

            if from_alert <= mynow.time() <= to_alert:
                self.display = True
            else:
                self.display = False
<|endoftext|>


<commit_before>import os
import shutil
import string
import tempfile
from threading import Timer
import subprocess

def memoize(f):
    rets = {}

    def wrap(*args):
        if not args in rets:
            rets[args] = f(*args)

        return rets[args]

    wrap.__name__ = f.__name__
    return wrap

def climb(top):
    right = True
    while right:
        top, right = os.path.split(top)
        yield top

@memoize
def find(top, name, parent=False):
    for d in climb(top):
        target = os.path.join(d, name)
        if os.path.exists(target):
            if parent:
                return d

            return target

def extract_path(cmd, delim=':'):
    path = popen(cmd, os.environ).communicate()[0].decode()
    path = path.split('__SUBL__', 1)[1].strip('\r\n')
    return ':'.join(path.split(delim))

def find_path(env):
    # find PATH using shell --login
    if 'SHELL' in env:
        shell_path = env['SHELL']
        shell = os.path.basename(shell_path)

        if shell in ('bash', 'zsh'):
            return extract_path(
                (shell_path, '--login', '-c', 'echo __SUBL__$PATH')
            )
        elif shell == 'fish':
            return extract_path(
                (shell_path, '--login', '-c', 'echo __SUBL__; for p in $PATH; echo $p; end'),
                '\n'
            )

    # guess PATH if we haven't returned yet
    split = env['PATH'].split(':')
    p = env['PATH']
    for path in (
        '/usr/bin', '/usr/local/bin',
        '/usr/local/php/bin', '/usr/local/php5/bin'
                ):
        if not path in split:
            p += (':' + path)

    return p

@memoize
def create_environment():
    if os.name == 'posix':
        os.environ['PATH'] = find_path(os.environ)

    return os.environ

def can_exec(fpath):
    return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

def which(cmd):
    env = create_environment()
    for base in env.get('PATH', '').split(os.pathsep):
        path = os.path.join(base, cmd)
        if can_exec(path):
            return path

    return None

def touch(path):
    with open(path, 'a'):
        os.utime(path, None)

# popen methods
def combine_output(out, sep=''):
    return sep.join((
        (out[0].decode('utf8') or ''),
        (out[1].decode('utf8') or ''),
    ))

def communicate(cmd, stdin=None, timeout=None, **popen_args):
    p = popen(cmd, **popen_args)
    if isinstance(p, subprocess.Popen):
        timer = None
        if timeout is not None:
            kill = lambda: p.kill()
            timer = Timer(timeout, kill)
            timer.start()

        out = p.communicate(stdin)
        if timer is not None:
            timer.cancel()

        return combine_output(out)
    elif isinstance(p, str):
        return p
    else:
        return ''

def tmpfile(cmd, code, suffix=''):
    if isinstance(cmd, str):
        cmd = cmd,

    f = tempfile.NamedTemporaryFile(suffix=suffix)
    f.write(code.encode('utf8'))
    f.flush()

    cmd = tuple(cmd) + (f.name,)
    out = popen(cmd)
    if out:
        out = out.communicate()
        return combine_output(out)
    else:
        return ''

def tmpdir(cmd, files, filename, code):
    filename = os.path.split(filename)[1]
    d = tempfile.mkdtemp()

    for f in files:
        try: os.makedirs(os.path.join(d, os.path.split(f)[0]))
        except: pass

        target = os.path.join(d, f)
        if os.path.split(target)[1] == filename:
            # source file hasn't been saved since change, so update it from our live buffer
            f = open(target, 'wb')
            f.write(code)
            f.close()
        else:
            shutil.copyfile(f, target)

    os.chdir(d)
    out = popen(cmd)
    if out:
        out = out.communicate()
        out = combine_output(out, '\n')

        # filter results from build to just this filename
        # no guarantee all languages are as nice about this as Go
        # may need to improve later or just defer to communicate()
        out = '\n'.join([
            line for line in out.split('\n') if filename in line.split(':', 1)[0]
        ])
    else:
        out = ''

    shutil.rmtree(d, True)
    return out

def popen(cmd, env=None, use_pty=False):
    if isinstance(cmd, str):
        cmd = cmd,

    stdin = subprocess.PIPE
    stdout = subprocess.PIPE
    stderr = subprocess.PIPE

    master = None
    info = None
    if os.name == 'nt':
        info = subprocess.STARTUPINFO()
        info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        info.wShowWindow = subprocess.SW_HIDE
    elif use_pty:
        import pty
        master, slave = pty.openpty()
        stdin = slave
        stdout = slave

    if env is None:
        env = create_environment()

    try:
        p = subprocess.Popen(cmd, stdin=stdin,
            stdout=stdout, stderr=stderr,
            startupinfo=info, env=env)

        if master:
            p.pty = True
            p.stdout = os.fdopen(master, 'rb')
            p.stdin = os.fdopen(master, 'wb')
        else:
            p.pty = False

        return p
    except OSError as err:
        print('Error launching', repr(cmd))
        print('Error was:', err.strerror)
        print('Environment:', env)
        return err.strerror

def get_windows_drives():
    from ctypes import windll
    bitmask = windll.kernel32.GetLogicalDrives()
    return [c for i, c in enumerate(string.ascii_uppercase) if bitmask & 1 << i]
<commit_msg>add .exe support to PATH search<commit_after>import os
import shutil
import string
import tempfile
from threading import Timer
import subprocess

def memoize(f):
    rets = {}

    def wrap(*args):
        if not args in rets:
            rets[args] = f(*args)

        return rets[args]

    wrap.__name__ = f.__name__
    return wrap

def climb(top):
    right = True
    while right:
        top, right = os.path.split(top)
        yield top

@memoize
def find(top, name, parent=False):
    for d in climb(top):
        target = os.path.join(d, name)
        if os.path.exists(target):
            if parent:
                return d

            return target

def extract_path(cmd, delim=':'):
    path = popen(cmd, os.environ).communicate()[0].decode()
    path = path.split('__SUBL__', 1)[1].strip('\r\n')
    return ':'.join(path.split(delim))

def find_path(env):
    # find PATH using shell --login
    if 'SHELL' in env:
        shell_path = env['SHELL']
        shell = os.path.basename(shell_path)

        if shell in ('bash', 'zsh'):
            return extract_path(
                (shell_path, '--login', '-c', 'echo __SUBL__$PATH')
            )
        elif shell == 'fish':
            return extract_path(
                (shell_path, '--login', '-c', 'echo __SUBL__; for p in $PATH; echo $p; end'),
                '\n'
            )

    # guess PATH if we haven't returned yet
    split = env['PATH'].split(':')
    p = env['PATH']
    for path in (
        '/usr/bin', '/usr/local/bin',
        '/usr/local/php/bin', '/usr/local/php5/bin'
                ):
        if not path in split:
            p += (':' + path)

    return p

@memoize
def create_environment():
    if os.name == 'posix':
        os.environ['PATH'] = find_path(os.environ)

    return os.environ

def can_exec(fpath):
    return os.path.isfile(fpath) and os.access(fpath, os.X_OK)

def which(cmd):
    env = create_environment()
    for base in env.get('PATH', '').split(os.pathsep):
        path = os.path.join(base, cmd)
        if can_exec(path):
            return path
        if os.name == 'nt':
            if not cmd.endswith('.exe') and can_exec(path + '.exe'):
                return path + '.exe'

    return None

def touch(path):
    with open(path, 'a'):
        os.utime(path, None)

# popen methods
def combine_output(out, sep=''):
    return sep.join((
        (out[0].decode('utf8') or ''),
        (out[1].decode('utf8') or ''),
    ))

def communicate(cmd, stdin=None, timeout=None, **popen_args):
    p = popen(cmd, **popen_args)
    if isinstance(p, subprocess.Popen):
        timer = None
        if timeout is not None:
            kill = lambda: p.kill()
            timer = Timer(timeout, kill)
            timer.start()

        out = p.communicate(stdin)
        if timer is not None:
            timer.cancel()

        return combine_output(out)
    elif isinstance(p, str):
        return p
    else:
        return ''

def tmpfile(cmd, code, suffix=''):
    if isinstance(cmd, str):
        cmd = cmd,

    f = tempfile.NamedTemporaryFile(suffix=suffix)
    f.write(code.encode('utf8'))
    f.flush()

    cmd = tuple(cmd) + (f.name,)
    out = popen(cmd)
    if out:
        out = out.communicate()
        return combine_output(out)
    else:
        return ''

def tmpdir(cmd, files, filename, code):
    filename = os.path.split(filename)[1]
    d = tempfile.mkdtemp()

    for f in files:
        try: os.makedirs(os.path.join(d, os.path.split(f)[0]))
        except: pass

        target = os.path.join(d, f)
        if os.path.split(target)[1] == filename:
            # source file hasn't been saved since change, so update it from our live buffer
            f = open(target, 'wb')
            f.write(code)
            f.close()
        else:
            shutil.copyfile(f, target)

    os.chdir(d)
    out = popen(cmd)
    if out:
        out = out.communicate()
        out = combine_output(out, '\n')

        # filter results from build to just this filename
        # no guarantee all languages are as nice about this as Go
        # may need to improve later or just defer to communicate()
        out = '\n'.join([
            line for line in out.split('\n') if filename in line.split(':', 1)[0]
        ])
    else:
        out = ''

    shutil.rmtree(d, True)
    return out

def popen(cmd, env=None, use_pty=False):
    if isinstance(cmd, str):
        cmd = cmd,

    stdin = subprocess.PIPE
    stdout = subprocess.PIPE
    stderr = subprocess.PIPE

    master = None
    info = None
    if os.name == 'nt':
        info = subprocess.STARTUPINFO()
        info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        info.wShowWindow = subprocess.SW_HIDE
    elif use_pty:
        import pty
        master, slave = pty.openpty()
        stdin = slave
        stdout = slave

    if env is None:
        env = create_environment()

    try:
        p = subprocess.Popen(cmd, stdin=stdin,
            stdout=stdout, stderr=stderr,
            startupinfo=info, env=env)

        if master:
            p.pty = True
            p.stdout = os.fdopen(master, 'rb')
            p.stdin = os.fdopen(master, 'wb')
        else:
            p.pty = False

        return p
    except OSError as err:
        print('Error launching', repr(cmd))
        print('Error was:', err.strerror)
        print('Environment:', env)
        return err.strerror

def get_windows_drives():
    from ctypes import windll
    bitmask = windll.kernel32.GetLogicalDrives()
    return [c for i, c in enumerate(string.ascii_uppercase) if bitmask & 1 << i]
<|endoftext|>


<commit_before># Copyright (c) 2015. Mount Sinai School of Medicine
#
# 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.

"""
Common commandline arguments used by scripts
"""

from __future__ import print_function, division, absolute_import
import argparse
import logging

import mhctools
from mhctools.alleles import normalize_allele_name
from pyensembl import genome_for_reference_name
import varcode

from .parsing_helpers import parse_int_list
from .rna import (
    load_cufflinks_fpkm_dict,
    load_transcript_fpkm_dict_from_gtf
)

arg_parser = argparse.ArgumentParser()

#
# Genomic Variants
#
variant_arg_group = arg_parser.add_argument_group(
    title="Variants",
    description="Genomic variant files")

variant_arg_group.add_argument(
    "--vcf",
    default=[],
    action="append",
    help="Genomic variants in VCF format")

variant_arg_group.add_argument(
    "--maf",
    default=[],
    action="append",
    help="Genomic variants in TCGA's MAF format",)

variant_arg_group.add_argument(
    "--variant",
    default=[],
    action="append",
    nargs=4,
    metavar=("CHR", "POS", "REF", "ALT"),
    help="Individual variant as 4 arguments giving chromsome, position, ref, "
    "and alt. Example: chr1 3848 C G. Use '.' to indicate empty alleles for "
    "insertions or deletions.")

variant_arg_group.add_argument(
    "--reference-name",
    type=str,
    help=(
        "What reference assembly your variant coordinates are using. "
        "Examples: 'hg19', 'GRCh38', or 'mm9'. "
        "This argument is ignored for MAF files, since each row includes "
        "the reference. "
        "For VCF files, this is used if specified, and otherwise is guessed from "
        "the header. For variants specfied on the commandline with --variant, "
        "this option is required."))

variant_arg_group.add_argument(
    "--json-variant-files",
    default=[],
    action="append",
    help="Path to Varcode.VariantCollection object serialized as a JSON file.")

def variant_collection_from_args(args):
    variant_collections = []

    if args.reference_name:
        genome = genome_for_reference_name(args.reference_name)
    else:
        # no genome specified, assume it can be inferred from the file(s)
        # we're loading
        genome = None

    for vcf_path in args.vcf:
        vcf_variants = varcode.load_vcf(vcf_path, genome=genome)
        variant_collections.append(vcf_variants)
    for maf_path in args.maf:
        maf_variants = varcode.load_maf(maf_path)
        variant_collections.append(maf_variants)

    if args.variant:
        if not genome:
            raise ValueError(
                "--reference-name must be specified when using --variant")

        variants = [
            varcode.Variant(
                chromosome,
                start=position,
                ref=ref,
                alt=alt,
                ensembl=genome)
            for (chromosome, position, ref, alt)
            in args.variant
        ]
        variant_collection = varcode.VariantCollection(variants)
        variant_collections.append(variant_collection)

    if len(variant_collections) == 0:
        raise ValueError(
            "No variants loaded (use --maf, --vcf, or --variant options)")

    for json_path in args.json_variant_files:
        with open(json_path, 'r') as f:
            json_string = f.read()
            variant_collections.append(
                varcode.VariantCollection.from_json(json_string))
    if len(variant_collections) == 0:
        raise ValueError(
            "No variants loaded (use --maf, --vcf, --json-variants options)")
    elif len(variant_collections) == 1:
        return variant_collections[0]
    else:
        combined_variants = []
        for variant_collection in variant_collections:
            combined_variants.extend(list(variant_collection))
        return varcode.VariantCollection(combined_variants)

#
# Options passed to MHC binding predictor
#
mhc_options_arg_group = arg_parser.add_argument_group(
    title="MHC Prediction Options",
    description="MHC Binding Prediction Options")

mhc_options_arg_group.add_argument(
    "--mhc-epitope-lengths",
    default=[8, 9, 10, 11],
    type=parse_int_list,
    help="Lengths of epitopes to consider for MHC binding prediction")

arg_parser.add_argument(
    "--mhc-alleles-file",
    help="File with one HLA allele per line")

arg_parser.add_argument(
    "--mhc-alleles",
    default="",
    help="Comma separated list of allele (default HLA-A*02:01)")

def mhc_alleles_from_args(args):
    alleles = [
        normalize_allele_name(allele.strip())
        for allele in args.mhc_alleles.split(",")
        if allele.strip()
    ]
    if args.mhc_alleles_file:
        with open(args.mhc_alleles_file, 'r') as f:
            for line in f:
                line = line.strip()
                if line:
                    alleles.append(normalize_allele_name(line))
    if len(alleles) == 0:
        raise ValueError(
            "MHC alleles required (use --mhc-alleles or --mhc-alleles-file)")
    return alleles

#
# MHC Binding Prediction
#

mhc_predictors = {
    "netmhc": mhctools.NetMHC,
    "netmhcpan": mhctools.NetMHCpan,
    "netmhciipan": mhctools.NetMHCIIpan,
    "netmhccons": mhctools.NetMHCcons,
    "random": mhctools.RandomBindingPredictor,
    # TODO implement SMM predictors in mhctools
    "smm": None,
    "smm-pmbec": None,
    # use NetMHCpan via IEDB's web API
    "netmhcpan-iedb": mhctools.IedbNetMHCpan,
    # use NetMHCcons via IEDB's web API
    "netmhccons-iedb": mhctools.IedbNetMHCcons,
    # use SMM via IEDB's web API
    "smm-iedb": mhctools.IedbSMM,
    # use SMM-PMBEC via IEDB's web API
    "smm-pmbec-iedb": mhctools.IedbSMM_PMBEC,
    # Class II MHC binding prediction using NetMHCIIpan via IEDB
    "netmhciipan-iedb": mhctools.IedbNetMHCIIpan,
}

mhc_predictor_arg_group = arg_parser.add_argument(
    "--mhc-predictor",
    choices=list(sorted(mhc_predictors.keys())),
    type=lambda s: s.lower().strip(),
    required=True)

def mhc_binding_predictor_from_args(args):
    mhc_class = mhc_predictors.get(args.mhc_predictor)
    if mhc_class is None:
        raise ValueError(
            "Invalid MHC prediction method: %s" % (args.mhc_predictor,))
    alleles = mhc_alleles_from_args(args)
    epitope_lengths = args.mhc_epitope_lengths
    logging.info(
        ("Building MHC binding prediction %s"
         " for alleles %s"
         " and epitope lengths %s") % (
            mhc_class.__class__.__name__,
            alleles,
            epitope_lengths))
    return mhc_class(
        alleles=alleles,
        epitope_lengths=epitope_lengths)
#
# Mutated sequence options
#
arg_parser.add_argument(
    "--padding-around-mutation",
    default=None,
    help="".join([
        "How many extra amino acids to include on either side of a mutation.",
        "Default is determined by epitope lengths but can be overridden to ",
        "predict wildtype epitopes in a larger context around a mutant residue.",
    ]),
    type=int)

###
# RNA-Seq data
###

rna_group = arg_parser.add_argument_group(
    title="RNA-Seq",
    description="Transcript and gene abundance quantification")

rna_group.add_argument(
    "--rna-transcript-fpkm-tracking-file",
    help="".join([
        "Cufflinks tracking file (FPKM estimates for Ensembl transcripts). ",
        "Used both for expression filtering and selecting the most abundant ",
        "transcript to use for determining a mutant protein sequence."]))

rna_group.add_argument(
    "--rna-transcript-fpkm-gtf-file",
    help="".join([
        "GTF file containing FPKM estimates for Ensembl transcripts.",
        "Used both for expression filtering and selecting the most abundant ",
        "transcript to use for determining a mutant protein sequence."]))

rna_group.add_argument(
    "--rna-min-transcript-expression",
    help="Minimum FPKM for transcript expression",
    default=0.0,
    type=float)

rna_group.add_argument(
    "--rna-gene-fpkm-tracking-file",
    help="Cufflinks tracking file (FPKM estimates for Ensembl genes)",
    required=False)

rna_group.add_argument(
    "--rna-min-gene-expression",
    help="Minimum FPKM for gene expression",
    default=0.0,
    type=float)

def rna_gene_expression_dict_from_args(args):
    """
    Returns a dictionary mapping Ensembl gene IDs to FPKM expression values
    or None if neither Cufflinks tracking file nor StringTie GTF file specified
    in the commandline arguments.
    """
    if args.rna_gene_fpkm_tracking_file:
        return load_cufflinks_fpkm_dict(args.rna_gene_fpkm_file)
    else:
        return None

def rna_transcript_expression_dict_from_args(args):
    """
    Returns a dictionary mapping Ensembl transcript IDs to FPKM expression
    values or None if neither Cufflinks tracking file nor StringTie GTF file
    were specified.
    """
    if args.rna_transcript_fpkm_tracking_file:
        return load_cufflinks_fpkm_dict(args.rna_transcript_fpkm_tracking_file)
    elif args.rna_transcript_fpkm_gtf_file:
        return load_transcript_fpkm_dict_from_gtf(
            args.rna_transcript_fpkm_tracking_file)
    else:
        return None

# TODO:
# --rna-read-evidence-bam flag for filtering by Varcode.read_evidence
# --rna-min-read-evidence minimum number of RNA reads containing variant allele
#
# Filtering of epitopes
#
filter_group = arg_parser.add_argument_group(
    title="Filtering Options",
    description="Criteria for removing epitopes from results")

filter_group.add_argument(
    "--ic50-cutoff",
    help="Drop epitopes with predicted IC50 nM affinity above this value",
    default=None,
    type=float)

filter_group.add_argument(
    "--percentile-cutoff",
    help="Drop epitopes with predicted IC50 percentile rank above this value",
    default=None,
    type=float)

filter_group.add_argument(
    "--only-novel-epitopes",
    help="".join([
        "Drop epitopes which do not contain mutated residues or occur ",
        "in the self-ligandome."]),
    default=False,
    action="store_true")

filter_group.add_argument(
    "--wildtype-ligandome-directory",
    help="".join([
        "Directory of 'self' ligand peptide sets, in files named ",
        "by allele (e.g. 'A0201'). Any predicted mutant epitope which ",
        "is in the files associated with the given alleles is treated as ",
        "wildtype (non-mutated)."]))

#
# Misc
#
arg_parser.add_argument(
    "--skip-variant-errors",
    default=False,
    action="store_true",
    help="Skip variants which cause errors")

#
# Output
#

output_group = arg_parser.add_argument_group(
    title="Output",
    description="Write results to different formats")

output_group.add_argument(
    "--output-csv",
    default=None,
    help="Path to output CSV file")

output_group.add_argument(
    "--output-html",
    default=None,
    help="Path to output HTML file")
<commit_msg>passing wrong arg as file path<commit_after># Copyright (c) 2015. Mount Sinai School of Medicine
#
# 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.

"""
Common commandline arguments used by scripts
"""

from __future__ import print_function, division, absolute_import
import argparse
import logging

import mhctools
from mhctools.alleles import normalize_allele_name
from pyensembl import genome_for_reference_name
import varcode

from .parsing_helpers import parse_int_list
from .rna import (
    load_cufflinks_fpkm_dict,
    load_transcript_fpkm_dict_from_gtf
)

arg_parser = argparse.ArgumentParser()

#
# Genomic Variants
#
variant_arg_group = arg_parser.add_argument_group(
    title="Variants",
    description="Genomic variant files")

variant_arg_group.add_argument(
    "--vcf",
    default=[],
    action="append",
    help="Genomic variants in VCF format")

variant_arg_group.add_argument(
    "--maf",
    default=[],
    action="append",
    help="Genomic variants in TCGA's MAF format",)

variant_arg_group.add_argument(
    "--variant",
    default=[],
    action="append",
    nargs=4,
    metavar=("CHR", "POS", "REF", "ALT"),
    help="Individual variant as 4 arguments giving chromsome, position, ref, "
    "and alt. Example: chr1 3848 C G. Use '.' to indicate empty alleles for "
    "insertions or deletions.")

variant_arg_group.add_argument(
    "--reference-name",
    type=str,
    help=(
        "What reference assembly your variant coordinates are using. "
        "Examples: 'hg19', 'GRCh38', or 'mm9'. "
        "This argument is ignored for MAF files, since each row includes "
        "the reference. "
        "For VCF files, this is used if specified, and otherwise is guessed from "
        "the header. For variants specfied on the commandline with --variant, "
        "this option is required."))

variant_arg_group.add_argument(
    "--json-variant-files",
    default=[],
    action="append",
    help="Path to Varcode.VariantCollection object serialized as a JSON file.")

def variant_collection_from_args(args):
    variant_collections = []

    if args.reference_name:
        genome = genome_for_reference_name(args.reference_name)
    else:
        # no genome specified, assume it can be inferred from the file(s)
        # we're loading
        genome = None

    for vcf_path in args.vcf:
        vcf_variants = varcode.load_vcf(vcf_path, genome=genome)
        variant_collections.append(vcf_variants)
    for maf_path in args.maf:
        maf_variants = varcode.load_maf(maf_path)
        variant_collections.append(maf_variants)

    if args.variant:
        if not genome:
            raise ValueError(
                "--reference-name must be specified when using --variant")

        variants = [
            varcode.Variant(
                chromosome,
                start=position,
                ref=ref,
                alt=alt,
                ensembl=genome)
            for (chromosome, position, ref, alt)
            in args.variant
        ]
        variant_collection = varcode.VariantCollection(variants)
        variant_collections.append(variant_collection)

    if len(variant_collections) == 0:
        raise ValueError(
            "No variants loaded (use --maf, --vcf, or --variant options)")

    for json_path in args.json_variant_files:
        with open(json_path, 'r') as f:
            json_string = f.read()
            variant_collections.append(
                varcode.VariantCollection.from_json(json_string))
    if len(variant_collections) == 0:
        raise ValueError(
            "No variants loaded (use --maf, --vcf, --json-variants options)")
    elif len(variant_collections) == 1:
        return variant_collections[0]
    else:
        combined_variants = []
        for variant_collection in variant_collections:
            combined_variants.extend(list(variant_collection))
        return varcode.VariantCollection(combined_variants)

#
# Options passed to MHC binding predictor
#
mhc_options_arg_group = arg_parser.add_argument_group(
    title="MHC Prediction Options",
    description="MHC Binding Prediction Options")

mhc_options_arg_group.add_argument(
    "--mhc-epitope-lengths",
    default=[8, 9, 10, 11],
    type=parse_int_list,
    help="Lengths of epitopes to consider for MHC binding prediction")

arg_parser.add_argument(
    "--mhc-alleles-file",
    help="File with one HLA allele per line")

arg_parser.add_argument(
    "--mhc-alleles",
    default="",
    help="Comma separated list of allele (default HLA-A*02:01)")

def mhc_alleles_from_args(args):
    alleles = [
        normalize_allele_name(allele.strip())
        for allele in args.mhc_alleles.split(",")
        if allele.strip()
    ]
    if args.mhc_alleles_file:
        with open(args.mhc_alleles_file, 'r') as f:
            for line in f:
                line = line.strip()
                if line:
                    alleles.append(normalize_allele_name(line))
    if len(alleles) == 0:
        raise ValueError(
            "MHC alleles required (use --mhc-alleles or --mhc-alleles-file)")
    return alleles

#
# MHC Binding Prediction
#

mhc_predictors = {
    "netmhc": mhctools.NetMHC,
    "netmhcpan": mhctools.NetMHCpan,
    "netmhciipan": mhctools.NetMHCIIpan,
    "netmhccons": mhctools.NetMHCcons,
    "random": mhctools.RandomBindingPredictor,
    # TODO implement SMM predictors in mhctools
    "smm": None,
    "smm-pmbec": None,
    # use NetMHCpan via IEDB's web API
    "netmhcpan-iedb": mhctools.IedbNetMHCpan,
    # use NetMHCcons via IEDB's web API
    "netmhccons-iedb": mhctools.IedbNetMHCcons,
    # use SMM via IEDB's web API
    "smm-iedb": mhctools.IedbSMM,
    # use SMM-PMBEC via IEDB's web API
    "smm-pmbec-iedb": mhctools.IedbSMM_PMBEC,
    # Class II MHC binding prediction using NetMHCIIpan via IEDB
    "netmhciipan-iedb": mhctools.IedbNetMHCIIpan,
}

mhc_predictor_arg_group = arg_parser.add_argument(
    "--mhc-predictor",
    choices=list(sorted(mhc_predictors.keys())),
    type=lambda s: s.lower().strip(),
    required=True)

def mhc_binding_predictor_from_args(args):
    mhc_class = mhc_predictors.get(args.mhc_predictor)
    if mhc_class is None:
        raise ValueError(
            "Invalid MHC prediction method: %s" % (args.mhc_predictor,))
    alleles = mhc_alleles_from_args(args)
    epitope_lengths = args.mhc_epitope_lengths
    logging.info(
        ("Building MHC binding prediction %s"
         " for alleles %s"
         " and epitope lengths %s") % (
            mhc_class.__class__.__name__,
            alleles,
            epitope_lengths))
    return mhc_class(
        alleles=alleles,
        epitope_lengths=epitope_lengths)
#
# Mutated sequence options
#
arg_parser.add_argument(
    "--padding-around-mutation",
    default=None,
    help="".join([
        "How many extra amino acids to include on either side of a mutation.",
        "Default is determined by epitope lengths but can be overridden to ",
        "predict wildtype epitopes in a larger context around a mutant residue.",
    ]),
    type=int)

###
# RNA-Seq data
###

rna_group = arg_parser.add_argument_group(
    title="RNA-Seq",
    description="Transcript and gene abundance quantification")

rna_group.add_argument(
    "--rna-transcript-fpkm-tracking-file",
    help="".join([
        "Cufflinks tracking file (FPKM estimates for Ensembl transcripts). ",
        "Used both for expression filtering and selecting the most abundant ",
        "transcript to use for determining a mutant protein sequence."]))

rna_group.add_argument(
    "--rna-transcript-fpkm-gtf-file",
    help="".join([
        "GTF file containing FPKM estimates for Ensembl transcripts.",
        "Used both for expression filtering and selecting the most abundant ",
        "transcript to use for determining a mutant protein sequence."]))

rna_group.add_argument(
    "--rna-min-transcript-expression",
    help="Minimum FPKM for transcript expression",
    default=0.0,
    type=float)

rna_group.add_argument(
    "--rna-gene-fpkm-tracking-file",
    help="Cufflinks tracking file (FPKM estimates for Ensembl genes)",
    required=False)

rna_group.add_argument(
    "--rna-min-gene-expression",
    help="Minimum FPKM for gene expression",
    default=0.0,
    type=float)

def rna_gene_expression_dict_from_args(args):
    """
    Returns a dictionary mapping Ensembl gene IDs to FPKM expression values
    or None if neither Cufflinks tracking file nor StringTie GTF file specified
    in the commandline arguments.
    """
    if args.rna_gene_fpkm_tracking_file:
        return load_cufflinks_fpkm_dict(args.rna_gene_fpkm_file)
    else:
        return None

def rna_transcript_expression_dict_from_args(args):
    """
    Returns a dictionary mapping Ensembl transcript IDs to FPKM expression
    values or None if neither Cufflinks tracking file nor StringTie GTF file
    were specified.
    """
    if args.rna_transcript_fpkm_tracking_file:
        return load_cufflinks_fpkm_dict(args.rna_transcript_fpkm_tracking_file)
    elif args.rna_transcript_fpkm_gtf_file:
        return load_transcript_fpkm_dict_from_gtf(
            args.rna_transcript_fpkm_gtf_file)
    else:
        return None

# TODO:
# --rna-read-evidence-bam flag for filtering by Varcode.read_evidence
# --rna-min-read-evidence minimum number of RNA reads containing variant allele
#
# Filtering of epitopes
#
filter_group = arg_parser.add_argument_group(
    title="Filtering Options",
    description="Criteria for removing epitopes from results")

filter_group.add_argument(
    "--ic50-cutoff",
    help="Drop epitopes with predicted IC50 nM affinity above this value",
    default=None,
    type=float)

filter_group.add_argument(
    "--percentile-cutoff",
    help="Drop epitopes with predicted IC50 percentile rank above this value",
    default=None,
    type=float)

filter_group.add_argument(
    "--only-novel-epitopes",
    help="".join([
        "Drop epitopes which do not contain mutated residues or occur ",
        "in the self-ligandome."]),
    default=False,
    action="store_true")

filter_group.add_argument(
    "--wildtype-ligandome-directory",
    help="".join([
        "Directory of 'self' ligand peptide sets, in files named ",
        "by allele (e.g. 'A0201'). Any predicted mutant epitope which ",
        "is in the files associated with the given alleles is treated as ",
        "wildtype (non-mutated)."]))

#
# Misc
#
arg_parser.add_argument(
    "--skip-variant-errors",
    default=False,
    action="store_true",
    help="Skip variants which cause errors")

#
# Output
#

output_group = arg_parser.add_argument_group(
    title="Output",
    description="Write results to different formats")

output_group.add_argument(
    "--output-csv",
    default=None,
    help="Path to output CSV file")

output_group.add_argument(
    "--output-html",
    default=None,
    help="Path to output HTML file")
<|endoftext|>


<commit_before>#!/usr/bin/python2.5
#
# Copyright 2008 the Melange authors.
#
# 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.

"""Views for Clubs.
"""

__authors__ = [
    '"Lennard de Rijk" <ljvderijk@gmail.com>',
  ]

from google.appengine.api import users

from django import forms

from soc.logic import dicts
from soc.logic.models import user as user_logic
from soc.views.helper import widgets
from soc.views.models import base

import soc.logic.models.club

class View(base.View):
  """View methods for the Club model.
  """

  def __init__(self, params=None):
    """Defines the fields and methods required for the base View class
    to provide the user with list, public, create, edit and delete views.

    Params:
      params: a dict with params for this View
    """

    new_params = {}

    new_params['logic'] = soc.logic.models.club.logic

    new_params['name'] = "Club"
    new_params['name_short'] = "Club"
    new_params['name_plural'] = "Clubs"
    new_params['url_name'] = "club"
    new_params['module_name'] = "club"

    new_params['extra_dynaexclude'] = ['founder', 'home']
    new_params['edit_extra_dynafields'] = {
        'founded_by': forms.CharField(widget=widgets.ReadOnlyInput(),
                                   required=False),
        }

    # TODO(tlarsen): Add support for Django style template lookup
    new_params['public_template'] = 'soc/club/public.html'

    new_params['list_row'] = 'soc/club/list/row.html'
    new_params['list_heading'] = 'soc/club/list/heading.html'

    params = dicts.merge(params, new_params)

    super(View, self).__init__(params=params)

  def _editGet(self, request, entity, form):
    """See base.View._editGet().
    """

    # fill in the founded_by with data from the entity
    form.fields['founded_by'].initial = entity.founder.name
    super(View, self)._editGet(request, entity, form)

  def _editPost(self, request, entity, fields):
    """See base.View._editPost().
    """

    if not entity:
      # only if we are creating a new entity we should fill in founder
      account = users.get_current_user()
      user = user_logic.logic.getForFields({'account': account}, unique=True)
      fields['founder'] = user

    super(View, self)._editPost(request, entity, fields)

view = View()

create = view.create
delete = view.delete
edit = view.edit
list = view.list
public = view.public<commit_msg>Removed unneeded params from club<commit_after>#!/usr/bin/python2.5
#
# Copyright 2008 the Melange authors.
#
# 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.

"""Views for Clubs.
"""

__authors__ = [
    '"Lennard de Rijk" <ljvderijk@gmail.com>',
  ]

from google.appengine.api import users

from django import forms

from soc.logic import dicts
from soc.logic.models import user as user_logic
from soc.views.helper import widgets
from soc.views.models import base

import soc.logic.models.club

class View(base.View):
  """View methods for the Club model.
  """

  def __init__(self, params=None):
    """Defines the fields and methods required for the base View class
    to provide the user with list, public, create, edit and delete views.

    Params:
      params: a dict with params for this View
    """

    new_params = {}

    new_params['logic'] = soc.logic.models.club.logic

    new_params['name'] = "Club"
    new_params['name_short'] = "Club"
    new_params['name_plural'] = "Clubs"
    new_params['url_name'] = "club"
    new_params['module_name'] = "club"

    new_params['extra_dynaexclude'] = ['founder', 'home']
    new_params['edit_extra_dynafields'] = {
        'founded_by': forms.CharField(widget=widgets.ReadOnlyInput(),
                                   required=False),
        }

    params = dicts.merge(params, new_params)

    super(View, self).__init__(params=params)

  def _editGet(self, request, entity, form):
    """See base.View._editGet().
    """

    # fill in the founded_by with data from the entity
    form.fields['founded_by'].initial = entity.founder.name
    super(View, self)._editGet(request, entity, form)

  def _editPost(self, request, entity, fields):
    """See base.View._editPost().
    """

    if not entity:
      # only if we are creating a new entity we should fill in founder
      account = users.get_current_user()
      user = user_logic.logic.getForFields({'account': account}, unique=True)
      fields['founder'] = user

    super(View, self)._editPost(request, entity, fields)

view = View()

create = view.create
delete = view.delete
edit = view.edit
list = view.list
public = view.public<|endoftext|>


<commit_before>#!/usr/bin/python
# -*- encoding: utf-8 -*-
"""

"""
from collections import namedtuple
import inspect
import logging
from sqlalchemy import inspect as sqinspect
from sqlalchemy.exc import NoInspectionAvailable
from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import ColumnProperty, Query
from sqlalchemy.orm.attributes import QueryableAttribute
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.interfaces import MapperProperty
from sqlalchemy.orm.properties import RelationshipProperty
from sqlalchemy.sql.operators import is_ordering_modifier
from sqlalchemy.util import memoized_property

__author__ = 'Martin Martimeo <martin@martimeo.de>'
__date__ = '27.04.13 - 00:14'

def _filter(instance, condition) -> dict:
    """
        Filter properties of instace based on condition

        :param instance:
        :param condition:
        :rtype: dict
    """

    # Use iterate_properties when available
    if hasattr(instance, 'iterate_properties'):
        return {field.key: field for field in instance.iterate_properties
                if condition(field)}

    # Try sqlalchemy inspection
    try:
        return {field.key: field for key, field in sqinspect(instance).all_orm_descriptors.items()
                if condition(field)}

    # Use Inspect
    except NoInspectionAvailable:
        return {field.key: field for key, field in inspect.getmembers(instance)
                if condition(field)}

def _is_ordering_expression(expression):
    """
        Test an expression whether it is an ordering clause
    """

    if hasattr(expression, 'operator') and is_ordering_modifier(expression.operator):
        return True

    if hasattr(expression, 'modifier') and is_ordering_modifier(expression.modifier):
        return True

    return False

class ModelWrapper(object):
    """
        Wrapper around sqlalchemy model for having some easier functions
    """

    def __init__(self, model):
        self.model = model

    @property
    def __name__(self):
        return self.model.__name__

    @property
    def __tablename__(self):
        return self.model.__tablename__

    @property
    def __collectionname__(self):
        try:
            return self.model.__collectionname__
        except AttributeError:
            logging.warning("Missing collection name for %s using tablename" % self.model.__name__)
            return self.model.__tablename__

    @staticmethod
    def get_primary_keys(instance) -> dict:
        """
            Returns the primary keys

            Inspired by flask-restless.helpers.primary_key_names

            :param instance: Model ORM Instance
        """
        return _filter(instance, lambda field: isinstance(field, ColumnProperty) and field.primary_key or (
            isinstance(field, QueryableAttribute) and isinstance(field.property, ColumnProperty) and field.property.columns[0].primary_key))

    @memoized_property
    def primary_keys(self):
        """
        @see get_primary_keys
        """
        return self.get_primary_keys(self.model)

    @staticmethod
    def get_unique_keys(instance) -> dict:
        """
            Returns the primary keys

            Inspired by flask-restless.helpers.primary_key_names

            :param instance: Model ORM Instance
        """
        return _filter(instance, lambda field: isinstance(field, ColumnProperty) and field.unique or (
            isinstance(field, QueryableAttribute) and isinstance(field.property, ColumnProperty) and field.property.columns[0].unique))

    @memoized_property
    def unique_keys(self):
        """
        @see get_primary_keys
        """
        return self.get_unique_keys(self.model)

    @staticmethod
    def get_foreign_keys(instance) -> list:
        """
            Returns the foreign keys

            Inspired by flask-restless.helpers.primary_key_names

            :param instance: Model ORM Instance
        """
        return {field.key: field for key, field in inspect.getmembers(instance)
                if isinstance(field, QueryableAttribute)
                   and isinstance(field.property, ColumnProperty)
        and field.foreign_keys}

    @memoized_property
    def foreign_keys(self):
        """
        @see get_foreign_keys
        """
        return self.get_foreign_keys(self.model)

    @staticmethod
    def get_columns(instance) -> dict:
        """
            Returns the columns objects of the model

            :param instance: Model ORM Instance
        """
        return _filter(instance, lambda field: isinstance(field, ColumnProperty) or (
            isinstance(field, QueryableAttribute) and isinstance(field.property, ColumnProperty)))

    @memoized_property
    def columns(self):
        """
        @see get_columns
        """
        return self.get_columns(self.model)

    @staticmethod
    def get_attributes(instance) -> dict:
        """
            Returns the attributes of the model

            :param instance: Model ORM Instance
        """
        return _filter(instance,
                       lambda field: isinstance(field, MapperProperty) or isinstance(field, QueryableAttribute))

    @memoized_property
    def attributes(self):
        """
        @see get_attributes
        """
        return self.get_attributes(self.model)

    @staticmethod
    def get_relations(instance) -> dict:
        """
            Returns the relations objects of the model

            :param instance: Model ORM Instance
        """
        return _filter(instance, lambda field: isinstance(field, RelationshipProperty) or (
            isinstance(field, QueryableAttribute) and isinstance(field.property, RelationshipProperty)))

    @memoized_property
    def relations(self):
        """
        @see get_relations
        """
        return self.get_relations(self.model)

    @staticmethod
    def get_hybrids(instance) -> list:
        """
            Returns the relations objects of the model

            :param instance: Model ORM Instance
        """
        Proxy = namedtuple('Proxy', ['key', 'field'])
        if hasattr(instance, 'iterate_properties'):
            return [Proxy(key, field) for key, field in sqinspect(instance).all_orm_descriptors.items()
                    if isinstance(field, hybrid_property)]
        else:
            return [Proxy(key, field) for key, field in inspect.getmembers(instance)
                    if isinstance(field, hybrid_property)]

    @memoized_property
    def hybrids(self) -> list:
        """
        @see get_hybrids
        """
        return self.get_hybrids(self.model)

    @staticmethod
    def get_proxies(instance) -> list:
        """
            Returns the proxies objects of the model

            Inspired by https://groups.google.com/forum/?fromgroups=#!topic/sqlalchemy/aDi_M4iH7d0

            :param instance: Model ORM Instance
        """
        Proxy = namedtuple('Proxy', ['key', 'field'])
        if hasattr(instance, 'iterate_properties'):
            return [Proxy(key, field) for key, field in sqinspect(instance).all_orm_descriptors.items()
                    if isinstance(field, AssociationProxy)]
        else:
            return [Proxy(key, field) for key, field in inspect.getmembers(instance)
                    if isinstance(field, AssociationProxy)]

    @memoized_property
    def proxies(self):
        """
        @see get_proxies
        """
        return self.get_proxies(self.model)

class SessionedModelWrapper(ModelWrapper):
    """
        Wrapper around sqlalchemy model for having some easier functions
    """

    def __init__(self, model, session):
        super().__init__(model)
        self.session = session

    def one(self, offset: int=None, filters: list=()) -> object:
        """
            Gets one instance of the model filtered by filters

            :param offset: Offset for request
            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter_by(expression)
        if offset is not None:
            instance = instance.offset(offset)
        return instance.one()

    @staticmethod
    def get_all(instance: Query, offset: int=None, limit: int=None, filters: list=()) -> list:
        """
            Gets all instances of the query instance

            :param instance: sqlalchemy queriable
            :param offset: Offset for request
            :param limit: Limit for request
            :param filters: Filters and OrderBy Clauses
        """
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter(expression)
        if offset is not None:
            instance = instance.offset(offset)
        if limit is not None:
            instance = instance.limit(limit)
        return instance.all()

    def all(self, offset: int=None, limit: int=None, filters: list=()) -> list:
        """
            Gets all instances of the model filtered by filters

            :param offset: Offset for request
            :param limit: Limit for request
            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        return self.get_all(instance, offset=offset, limit=limit, filters=filters)

    def update(self, values: dict, offset: int=None, limit: int=None, filters: list=()) -> int:
        """
            Updates all instances of the model filtered by filters

            :param values: Dictionary of values
            :param offset: Offset for request
            :param limit: Limit for request
            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter_by(expression)
        if offset is not None:
            instance = instance.offset(offset)
        if limit is not None:
            instance = instance.limit(limit)
        return instance.update(values)

    def delete(self, offset: int=None, limit: int=None, filters: list=()) -> int:
        """
            Delete all instances of the model filtered by filters

            :param offset: Offset for request
            :param limit: Limit for request
            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter_by(expression)
        if offset is not None:
            instance = instance.offset(offset)
        if limit is not None:
            instance = instance.limit(limit)
        return instance.delete()

    def count(self, filters: list=()) -> int:
        """
            Gets the instance count

            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter(expression)
        return instance.count()

    def get(self, instance_id: list) -> object:
        """
            Gets one instance of the model based on primary_keys

            :param instance_id: list of primary_keys
        """
        if len(instance_id) == 1:
            primary_keys = instance_id[0]
        else:
            primary_keys = tuple(instance_id)
        instance = self.session.query(self.model).get(primary_keys)
        if not instance:
            raise NoResultFound("No row was found for get()")
        return instance

    def __call__(self, **kwargs):
        instance = self.model()
        for key, value in kwargs.items():
            setattr(instance, key, value)
        self.session.add(instance)
        return instance

<commit_msg>Make documentation of wrapper properties visible for sphinx<commit_after>#!/usr/bin/python
# -*- encoding: utf-8 -*-
"""

"""
from collections import namedtuple
import inspect
import logging
from sqlalchemy import inspect as sqinspect
from sqlalchemy.exc import NoInspectionAvailable
from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import ColumnProperty, Query
from sqlalchemy.orm.attributes import QueryableAttribute
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.interfaces import MapperProperty
from sqlalchemy.orm.properties import RelationshipProperty
from sqlalchemy.sql.operators import is_ordering_modifier
from sqlalchemy.util import memoized_property

__author__ = 'Martin Martimeo <martin@martimeo.de>'
__date__ = '27.04.13 - 00:14'

def _filter(instance, condition) -> dict:
    """
        Filter properties of instace based on condition

        :param instance:
        :param condition:
        :rtype: dict
    """

    # Use iterate_properties when available
    if hasattr(instance, 'iterate_properties'):
        return {field.key: field for field in instance.iterate_properties
                if condition(field)}

    # Try sqlalchemy inspection
    try:
        return {field.key: field for key, field in sqinspect(instance).all_orm_descriptors.items()
                if condition(field)}

    # Use Inspect
    except NoInspectionAvailable:
        return {field.key: field for key, field in inspect.getmembers(instance)
                if condition(field)}

def _is_ordering_expression(expression):
    """
        Test an expression whether it is an ordering clause
    """

    if hasattr(expression, 'operator') and is_ordering_modifier(expression.operator):
        return True

    if hasattr(expression, 'modifier') and is_ordering_modifier(expression.modifier):
        return True

    return False

class ModelWrapper(object):
    """
        Wrapper around sqlalchemy model for having some easier functions
    """

    def __init__(self, model):
        self.model = model

    @property
    def __name__(self):
        return self.model.__name__

    @property
    def __tablename__(self):
        return self.model.__tablename__

    @property
    def __collectionname__(self):
        try:
            return self.model.__collectionname__
        except AttributeError:
            logging.warning("Missing collection name for %s using tablename" % self.model.__name__)
            return self.model.__tablename__

    @staticmethod
    def get_primary_keys(instance) -> dict:
        """
            Returns the primary keys

            Inspired by flask-restless.helpers.primary_key_names
        """
        return _filter(instance, lambda field: isinstance(field, ColumnProperty) and field.primary_key or (
            isinstance(field, QueryableAttribute) and isinstance(field.property, ColumnProperty) and field.property.columns[0].primary_key))

    @memoized_property
    def primary_keys(self):
        """
        @see get_primary_keys
        """
        return self.get_primary_keys(self.model)

    primary_keys.__doc__ = get_primary_keys.__func__.__doc__

    @staticmethod
    def get_unique_keys(instance) -> dict:
        """
            Returns the primary keys

            Inspired by flask-restless.helpers.primary_key_names
        """
        return _filter(instance, lambda field: isinstance(field, ColumnProperty) and field.unique or (
            isinstance(field, QueryableAttribute) and isinstance(field.property, ColumnProperty) and field.property.columns[0].unique))

    @memoized_property
    def unique_keys(self):
        """
        @see get_primary_keys
        """
        return self.get_unique_keys(self.model)

    unique_keys.__doc__ = get_unique_keys.__func__.__doc__

    @staticmethod
    def get_foreign_keys(instance) -> list:
        """
            Returns the foreign keys

            Inspired by flask-restless.helpers.primary_key_names
        """
        return {field.key: field for key, field in inspect.getmembers(instance)
                if isinstance(field, QueryableAttribute)
                   and isinstance(field.property, ColumnProperty)
        and field.foreign_keys}

    @memoized_property
    def foreign_keys(self):
        """
        @see get_foreign_keys
        """
        return self.get_foreign_keys(self.model)

    foreign_keys.__doc__ = get_foreign_keys.__func__.__doc__

    @staticmethod
    def get_columns(instance) -> dict:
        """
            Returns the columns objects of the model
        """
        return _filter(instance, lambda field: isinstance(field, ColumnProperty) or (
            isinstance(field, QueryableAttribute) and isinstance(field.property, ColumnProperty)))

    @memoized_property
    def columns(self):
        """
        @see get_columns
        """
        return self.get_columns(self.model)

    columns.__doc__ = get_columns.__func__.__doc__

    @staticmethod
    def get_attributes(instance) -> dict:
        """
            Returns the attributes of the model
        """
        return _filter(instance,
                       lambda field: isinstance(field, MapperProperty) or isinstance(field, QueryableAttribute))

    @memoized_property
    def attributes(self):
        """
        @see get_attributes
        """
        return self.get_attributes(self.model)

    attributes.__doc__ = get_attributes.__func__.__doc__

    @staticmethod
    def get_relations(instance) -> dict:
        """
            Returns the relations objects of the model
        """
        return _filter(instance, lambda field: isinstance(field, RelationshipProperty) or (
            isinstance(field, QueryableAttribute) and isinstance(field.property, RelationshipProperty)))

    @memoized_property
    def relations(self):
        """
        @see get_relations
        """
        return self.get_relations(self.model)

    relations.__doc__ = get_relations.__func__.__doc__

    @staticmethod
    def get_hybrids(instance) -> list:
        """
            Returns the relations objects of the model
        """
        Proxy = namedtuple('Proxy', ['key', 'field'])
        if hasattr(instance, 'iterate_properties'):
            return [Proxy(key, field) for key, field in sqinspect(instance).all_orm_descriptors.items()
                    if isinstance(field, hybrid_property)]
        else:
            return [Proxy(key, field) for key, field in inspect.getmembers(instance)
                    if isinstance(field, hybrid_property)]

    @memoized_property
    def hybrids(self) -> list:
        """
        @see get_hybrids
        """
        return self.get_hybrids(self.model)

    hybrids.__doc__ = get_hybrids.__func__.__doc__

    @staticmethod
    def get_proxies(instance) -> list:
        """
            Returns the proxies objects of the model

            Inspired by https://groups.google.com/forum/?fromgroups=#!topic/sqlalchemy/aDi_M4iH7d0
        """
        Proxy = namedtuple('Proxy', ['key', 'field'])
        if hasattr(instance, 'iterate_properties'):
            return [Proxy(key, field) for key, field in sqinspect(instance).all_orm_descriptors.items()
                    if isinstance(field, AssociationProxy)]
        else:
            return [Proxy(key, field) for key, field in inspect.getmembers(instance)
                    if isinstance(field, AssociationProxy)]

    @memoized_property
    def proxies(self):
        """
        @see get_proxies
        """
        return self.get_proxies(self.model)

    proxies.__doc__ = get_proxies.__func__.__doc__

class SessionedModelWrapper(ModelWrapper):
    """
        Wrapper around sqlalchemy model for having some easier functions
    """

    def __init__(self, model, session):
        super().__init__(model)
        self.session = session

    def one(self, offset: int=None, filters: list=()) -> object:
        """
            Gets one instance of the model filtered by filters

            :param offset: Offset for request
            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter_by(expression)
        if offset is not None:
            instance = instance.offset(offset)
        return instance.one()

    @staticmethod
    def get_all(instance: Query, offset: int=None, limit: int=None, filters: list=()) -> list:
        """
            Gets all instances of the query instance

            :param instance: sqlalchemy queriable
            :param offset: Offset for request
            :param limit: Limit for request
            :param filters: Filters and OrderBy Clauses
        """
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter(expression)
        if offset is not None:
            instance = instance.offset(offset)
        if limit is not None:
            instance = instance.limit(limit)
        return instance.all()

    def all(self, offset: int=None, limit: int=None, filters: list=()) -> list:
        """
            Gets all instances of the model filtered by filters

            :param offset: Offset for request
            :param limit: Limit for request
            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        return self.get_all(instance, offset=offset, limit=limit, filters=filters)

    def update(self, values: dict, offset: int=None, limit: int=None, filters: list=()) -> int:
        """
            Updates all instances of the model filtered by filters

            :param values: Dictionary of values
            :param offset: Offset for request
            :param limit: Limit for request
            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter_by(expression)
        if offset is not None:
            instance = instance.offset(offset)
        if limit is not None:
            instance = instance.limit(limit)
        return instance.update(values)

    def delete(self, offset: int=None, limit: int=None, filters: list=()) -> int:
        """
            Delete all instances of the model filtered by filters

            :param offset: Offset for request
            :param limit: Limit for request
            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter_by(expression)
        if offset is not None:
            instance = instance.offset(offset)
        if limit is not None:
            instance = instance.limit(limit)
        return instance.delete()

    def count(self, filters: list=()) -> int:
        """
            Gets the instance count

            :param filters: Filters and OrderBy Clauses
        """
        instance = self.session.query(self.model)
        for expression in filters:
            if _is_ordering_expression(expression):
                instance = instance.order_by(expression)
            else:
                instance = instance.filter(expression)
        return instance.count()

    def get(self, instance_id: list) -> object:
        """
            Gets one instance of the model based on primary_keys

            :param instance_id: list of primary_keys
        """
        if len(instance_id) == 1:
            primary_keys = instance_id[0]
        else:
            primary_keys = tuple(instance_id)
        instance = self.session.query(self.model).get(primary_keys)
        if not instance:
            raise NoResultFound("No row was found for get()")
        return instance

    def __call__(self, **kwargs):
        instance = self.model()
        for key, value in kwargs.items():
            setattr(instance, key, value)
        self.session.add(instance)
        return instance

<|endoftext|>