pax_global_header 0000666 0000000 0000000 00000000064 12411220504 0014501 g ustar 00root root 0000000 0000000 52 comment=a64c9f2edbf92a3324949216696259e9415031e4
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/ 0000775 0000000 0000000 00000000000 12411220504 0020364 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/ 0000775 0000000 0000000 00000000000 12411220504 0021641 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0023740 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/ 0000775 0000000 0000000 00000000000 12411220504 0024327 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0026426 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobank/ 0000775 0000000 0000000 00000000000 12411220504 0025742 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobank/__init__.py 0000664 0000000 0000000 00000001426 12411220504 0030056 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobank import Boobank
__all__ = ['Boobank']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobank/boobank.py 0000664 0000000 0000000 00000044174 12411220504 0027741 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2009-2011 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import datetime, uuid
from dateutil.relativedelta import relativedelta
from dateutil.parser import parse as parse_date
from decimal import Decimal, InvalidOperation
from weboob.capabilities.base import empty
from weboob.capabilities.bank import CapBank, Account, Transaction
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Boobank']
class OfxFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'raw', 'amount', 'category')
TYPES_ACCTS = ['', 'CHECKING', 'SAVINGS', 'DEPOSIT', 'LOAN', 'MARKET', 'JOINT', 'CARD']
TYPES_TRANS = ['', 'DIRECTDEP', 'PAYMENT', 'CHECK', 'DEP', 'OTHER', 'ATM', 'POS', 'INT', 'FEE']
TYPES_CURRS = ['', 'EUR', 'CHF', 'USD']
balance = Decimal(0)
coming = Decimal(0)
def start_format(self, **kwargs):
account = kwargs['account']
self.balance = account.balance
self.coming = account.coming
self.output(u'OFXHEADER:100')
self.output(u'DATA:OFXSGML')
self.output(u'VERSION:102')
self.output(u'SECURITY:NONE')
self.output(u'ENCODING:USASCII')
self.output(u'CHARSET:1252')
self.output(u'COMPRESSION:NONE')
self.output(u'OLDFILEUID:NONE')
self.output(u'NEWFILEUID:%s\n' % uuid.uuid1())
self.output(u'0INFO')
self.output(u'%s113942ENG' % datetime.date.today().strftime('%Y%m%d'))
self.output(u'%s' % uuid.uuid1())
self.output(u'0INFOnull')
self.output(u'%s' % (account.currency or 'EUR'))
self.output(u'null')
self.output(u'null')
self.output(u'%s' % account.id)
try:
account_type = self.TYPES_ACCTS[account.type]
except IndexError:
account_type = ''
self.output(u'%s' % (account_type or 'CHECKING'))
self.output(u'null')
self.output(u'')
self.output(u'%s' % datetime.date.today().strftime('%Y%m%d'))
self.output(u'%s' % datetime.date.today().strftime('%Y%m%d'))
def format_obj(self, obj, alias):
if obj.type != 0:
result = u'%s\n' % self.TYPES_TRANS[obj.type]
else:
result = u'%s\n' % ('DEBIT' if obj.amount < 0 else 'CREDIT')
result += u'%s\n' % obj.date.strftime('%Y%m%d')
result += u'%s\n' % obj.amount
result += u'%s\n' % obj.unique_id()
if hasattr(obj, 'label') and not empty(obj.label):
result += u'%s' % obj.label.replace('&', '&')
else:
result += u'%s' % obj.raw.replace('&', '&')
return result
def flush(self):
self.output(u'')
self.output(u'%s' % self.balance)
self.output(u'%s' % datetime.date.today().strftime('%Y%m%d'))
try:
self.output(u'%s' % (self.balance + self.coming))
except TypeError:
self.output(u'%s' % self.balance)
self.output(u'%s' % datetime.date.today().strftime('%Y%m%d'))
self.output(u'')
class QifFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'raw', 'amount')
def start_format(self, **kwargs):
self.output(u'!Type:Bank')
def format_obj(self, obj, alias):
result = u'D%s\n' % obj.date.strftime('%d/%m/%y')
result += u'T%s\n' % obj.amount
if hasattr(obj, 'category') and not empty(obj.category):
result += u'N%s\n' % obj.category
result += u'M%s\n' % obj.raw
result += u'^'
return result
class PrettyQifFormatter(QifFormatter):
MANDATORY_FIELDS = ('id', 'date', 'raw', 'amount', 'category')
def start_format(self, **kwargs):
self.output(u'!Type:Bank')
def format_obj(self, obj, alias):
if hasattr(obj, 'rdate') and not empty(obj.rdate):
result = u'D%s\n' % obj.rdate.strftime('%d/%m/%y')
else:
result = u'D%s\n' % obj.date.strftime('%d/%m/%y')
result += u'T%s\n' % obj.amount
if hasattr(obj, 'category') and not empty(obj.category):
result += u'N%s\n' % obj.category
if hasattr(obj, 'label') and not empty(obj.label):
result += u'M%s\n' % obj.label
else:
result += u'M%s\n' % obj.raw
result += u'^'
return result
class TransactionsFormatter(IFormatter):
MANDATORY_FIELDS = ('date', 'label', 'amount')
TYPES = ['', 'Transfer', 'Order', 'Check', 'Deposit', 'Payback', 'Withdrawal', 'Card', 'Loan', 'Bank']
def start_format(self, **kwargs):
self.output(' Date Category Label Amount ')
self.output('------------+------------+---------------------------------------------------+-----------')
def format_obj(self, obj, alias):
if hasattr(obj, 'category') and obj.category:
_type = obj.category
else:
try:
_type = self.TYPES[obj.type]
except (IndexError, AttributeError):
_type = ''
label = obj.label
if not label and hasattr(obj, 'raw'):
label = obj.raw
date = obj.date.strftime('%Y-%m-%d') if not empty(obj.date) else ''
amount = obj.amount or Decimal('0')
return ' %s %s %s %s' % (self.colored('%-10s' % date, 'blue'),
self.colored('%-12s' % _type[:12], 'magenta'),
self.colored('%-50s' % label[:50], 'yellow'),
self.colored('%10.2f' % amount, 'green' if amount >= 0 else 'red'))
class TransferFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'origin', 'recipient', 'amount')
DISPLAYED_FIELDS = ('reason', )
def format_obj(self, obj, alias):
result = u'------- Transfer %s -------\n' % obj.fullid
result += u'Date: %s\n' % obj.date
result += u'Origin: %s\n' % obj.origin
result += u'Recipient: %s\n' % obj.recipient
result += u'Amount: %.2f\n' % obj.amount
if obj.reason:
result += u'Reason: %s\n' % obj.reason
return result
class InvestmentFormatter(IFormatter):
MANDATORY_FIELDS = ('label', 'quantity', 'unitvalue')
DISPLAYED_FIELDS = ('code', 'diff')
tot_valuation = Decimal(0)
tot_diff = Decimal(0)
def start_format(self, **kwargs):
self.output(' Label Code Quantity Unit Value Valuation diff ')
self.output('-------------------------------+--------+----------+-----------+-----------+--------')
def format_obj(self, obj, alias):
label = obj.label
if not empty(obj.diff):
diff = obj.diff
else:
diff = obj.valuation - (obj.quantity * obj.unitprice)
self.tot_diff += diff
self.tot_valuation += obj.valuation
return u' %s %s %s %s %s %s' % \
(self.colored('%-30s' % label[:30], 'red'),
self.colored('%-10s' % obj.code[:8], 'yellow') if not empty(obj.code) else ' ' * 10,
self.colored('%6d' % obj.quantity, 'yellow'),
self.colored('%11.2f' % obj.unitvalue, 'yellow'),
self.colored('%11.2f' % obj.valuation, 'yellow'),
self.colored('%8.2f' % diff, 'green' if diff >= 0 else 'red')
)
def flush(self):
self.output('-------------------------------+--------+----------+-----------+-----------+--------')
self.output(u' Total %s %s' %
(self.colored('%8.2f' % self.tot_valuation, 'yellow'),
self.colored('%8.2f' % self.tot_diff, 'green' if self.tot_diff >=0 else 'red')
))
self.tot_valuation = Decimal(0)
self.tot_diff = Decimal(0)
class RecipientListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'label')
def start_format(self, **kwargs):
self.output('Available recipients:')
def get_title(self, obj):
return obj.label
class AccountListFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'label', 'balance', 'coming')
tot_balance = Decimal(0)
tot_coming = Decimal(0)
def start_format(self, **kwargs):
self.output(' %s Account Balance Coming ' % ((' ' * 15) if not self.interactive else ''))
self.output('------------------------------------------%s+----------+----------' % (('-' * 15) if not self.interactive else ''))
def format_obj(self, obj, alias):
if alias is not None:
id = '%s (%s)' % (self.colored('%3s' % ('#' + alias), 'red', 'bold'),
self.colored(obj.backend, 'blue', 'bold'))
clean = '#%s (%s)' % (alias, obj.backend)
if len(clean) < 15:
id += (' ' * (15 - len(clean)))
else:
id = self.colored('%30s' % obj.fullid, 'red', 'bold')
balance = obj.balance or Decimal('0')
coming = obj.coming or Decimal('0')
result = u'%s %s %s %s' % (id,
self.colored('%-25s' % obj.label[:25], 'yellow'),
self.colored('%9.2f' % obj.balance, 'green' if balance >= 0 else 'red') if not empty(obj.balance) else ' ' * 9,
self.colored('%9.2f' % obj.coming, 'green' if coming >= 0 else 'red') if not empty(obj.coming) else '')
self.tot_balance += balance
self.tot_coming += coming
return result
def flush(self):
self.output(u'------------------------------------------%s+----------+----------' % (('-' * 15) if not self.interactive else ''))
self.output(u'%s Total %s %s' % (
(' ' * 15) if not self.interactive else '',
self.colored('%8.2f' % self.tot_balance, 'green' if self.tot_balance >= 0 else 'red'),
self.colored('%8.2f' % self.tot_coming, 'green' if self.tot_coming >= 0 else 'red'))
)
self.tot_balance = Decimal(0)
self.tot_coming = Decimal(0)
class Boobank(ReplApplication):
APPNAME = 'boobank'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Romain Bignon, Christophe Benz'
CAPS = CapBank
DESCRIPTION = "Console application allowing to list your bank accounts and get their balance, " \
"display accounts history and coming bank operations, and transfer money from an account to " \
"another (if available)."
SHORT_DESCRIPTION = "manage bank accounts"
EXTRA_FORMATTERS = {'account_list': AccountListFormatter,
'recipient_list': RecipientListFormatter,
'transfer': TransferFormatter,
'qif': QifFormatter,
'pretty_qif': PrettyQifFormatter,
'ofx': OfxFormatter,
'ops_list': TransactionsFormatter,
'investment_list': InvestmentFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'ls': 'account_list',
'list': 'account_list',
'transfer': 'transfer',
'history': 'ops_list',
'coming': 'ops_list',
'investment': 'investment_list',
}
COLLECTION_OBJECTS = (Account, Transaction, )
def _complete_account(self, exclude=None):
if exclude:
exclude = '%s@%s' % self.parse_id(exclude)
return [s for s in self._complete_object() if s != exclude]
def do_list(self, line):
"""
list [-U]
List accounts.
Use -U to disable sorting of results.
"""
return self.do_ls(line)
def show_history(self, command, line):
id, end_date = self.parse_command_args(line, 2, 1)
account = self.get_object(id, 'get_account', [])
if not account:
print('Error: account "%s" not found (Hint: try the command "list")' % id, file=self.stderr)
return 2
if end_date is not None:
try:
end_date = parse_date(end_date)
except ValueError:
print('"%s" is an incorrect date format (for example "%s")' % \
(end_date, (datetime.date.today() - relativedelta(months=1)).strftime('%Y-%m-%d')), file=self.stderr)
return 3
old_count = self.options.count
self.options.count = None
self.start_format(account=account)
for backend, transaction in self.do(command, account, backends=account.backend):
if end_date is not None and transaction.date < end_date:
break
self.format(transaction)
if end_date is not None:
self.options.count = old_count
def complete_history(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_account()
@defaultcount(10)
def do_history(self, line):
"""
history ID [END_DATE]
Display history of transactions.
If END_DATE is supplied, list all transactions until this date.
"""
return self.show_history('iter_history', line)
def complete_coming(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_account()
@defaultcount(10)
def do_coming(self, line):
"""
coming ID [END_DATE]
Display future transactions.
If END_DATE is supplied, show all transactions until this date.
"""
return self.show_history('iter_coming', line)
def complete_transfer(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_account()
if len(args) == 3:
return self._complete_account(args[1])
def do_transfer(self, line):
"""
transfer ACCOUNT [RECIPIENT AMOUNT [REASON]]
Make a transfer beetwen two account
- ACCOUNT the source account
- RECIPIENT the recipient
- AMOUNT amount to transfer
- REASON reason of transfer
If you give only the ACCOUNT parameter, it lists all the
available recipients for this account.
"""
id_from, id_to, amount, reason = self.parse_command_args(line, 4, 1)
account = self.get_object(id_from, 'get_account', [])
if not account:
print('Error: account %s not found' % id_from, file=self.stderr)
return 1
if not id_to:
self.objects = []
self.set_formatter('recipient_list')
self.set_formatter_header(u'Available recipients')
self.start_format()
for backend, recipient in self.do('iter_transfer_recipients', account.id, backends=account.backend):
self.cached_format(recipient)
return 0
id_to, backend_name_to = self.parse_id(id_to)
if account.backend != backend_name_to:
print("Transfer between different backends is not implemented", file=self.stderr)
return 4
try:
amount = Decimal(amount)
except (TypeError, ValueError, InvalidOperation):
print('Error: please give a decimal amount to transfer', file=self.stderr)
return 2
if self.interactive:
# Try to find the recipient label. It can be missing from
# recipients list, for example for banks which allow transfers to
# arbitrary recipients.
to = id_to
for backend, recipient in self.do('iter_transfer_recipients', account.id, backends=account.backend):
if recipient.id == id_to:
to = recipient.label
break
print('Amount: %s%s' % (amount, account.currency_text))
print('From: %s' % account.label)
print('To: %s' % to)
print('Reason: %s' % (reason or ''))
if not self.ask('Are you sure to do this transfer?', default=True):
return
self.start_format()
for backend, transfer in self.do('transfer', account.id, id_to, amount, reason, backends=account.backend):
self.format(transfer)
def do_investment(self, id):
"""
investment ID
Display investments of an account.
"""
account = self.get_object(id, 'get_account', [])
if not account:
print('Error: account "%s" not found (Hint: try the command "list")' % id, file=self.stderr)
return 2
self.start_format()
for backend, investment in self.do('iter_investment', account, backends=account.backend):
self.format(investment)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobathon/ 0000775 0000000 0000000 00000000000 12411220504 0026302 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobathon/__init__.py 0000664 0000000 0000000 00000001426 12411220504 0030416 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobathon import Boobathon
__all__ = ['Boobathon']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobathon/boobathon.py 0000664 0000000 0000000 00000065475 12411220504 0030650 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from datetime import datetime, timedelta
import re
import sys
from urlparse import urlsplit
from random import choice
from weboob.capabilities.content import CapContent
from weboob.tools.application.repl import ReplApplication
from weboob.tools.ordereddict import OrderedDict
__all__ = ['Boobathon']
class Task(object):
STATUS_NONE = 0
STATUS_PROGRESS = 1
STATUS_DONE = 2
def __init__(self, backend, capability):
self.backend = backend
self.capability = capability
self.status = self.STATUS_NONE
self.date = None
self.branch = u''
def __repr__(self):
return '' % (self.backend, self.capability)
class Member(object):
def __init__(self, id, name):
self.name = name
self.id = id
self.tasks = []
self.availabilities = u''
self.repository = None
self.hardware = u''
self.is_me = False
def shortname(self):
name = self.name
if len(name) > 20:
name = '%s..' % name[:18]
return name
class Event(object):
def __init__(self, name, backend):
self.my_id = backend.browser.get_userid()
self.name = 'wiki/weboob/%s' % name
self.description = None
self.date = None
self.begin = None
self.end = None
self.location = None
self.winner = None
self.backend = backend
self.members = OrderedDict()
self.load()
def get_me(self):
return self.members.get(self.backend.browser.get_userid(), None)
def currently_in_event(self):
if not self.date or not self.begin or not self.end:
return False
return self.begin < datetime.now() < self.end
def is_closed(self):
return self.end < datetime.now()
def format_duration(self):
if not self.begin or not self.end:
return None
delta = self.end - self.begin
return '%02d:%02d' % (delta.seconds/3600, delta.seconds%3600)
def check_time_coherence(self):
"""
Check if the end's day is before the begin's one, in
case it stops at the next day (eg. 15h->1h).
If it occures, add a day.
"""
if self.begin > self.end:
self.end = self.end + timedelta(1)
def load(self):
self.content = self.backend.get_content(self.name)
self.members.clear()
member = None
for line in self.content.content.split('\n'):
line = line.strip()
if line.startswith('h1. '):
self.title = line[4:]
elif line.startswith('h3=. '):
m = re.match('h3=. Event finished. Winner is "(.*)":/users/(\d+)\!', line)
if not m:
print >>self.stderr, 'Unable to parse h3=: %s' % line
continue
self.winner = Member(int(m.group(2)), m.group(1))
elif line.startswith('h2. '):
continue
elif line.startswith('h3. '):
m = re.match('h3. "(.*)":/users/(\d+)', line)
if not m:
print >>self.stderr, 'Unable to parse user "%s"' % line
continue
member = Member(int(m.group(2)), m.group(1))
if member.id == self.my_id:
member.is_me = True
if self.winner is not None and member.id == self.winner.id:
self.winner = member
self.members[member.id] = member
elif self.description is None and len(line) > 0 and line != '{{TOC}}':
self.description = line
elif line.startswith('* '):
m = re.match('\* \*(\w+)\*: (.*)', line)
if not m:
continue
key, value = m.group(1), m.group(2)
if member is None:
if key == 'Date':
self.date = self.parse_date(value)
elif key == 'Start' or key == 'Begin':
self.begin = self.parse_time(value)
elif key == 'End':
self.end = self.parse_time(value)
self.check_time_coherence()
elif key == 'Location':
self.location = value
else:
if key == 'Repository':
m = re.match('"(.*.git)":.*', value)
if m:
member.repository = m.group(1)
else:
member.repository = value
elif key == 'Hardware':
member.hardware = value
elif key == 'Availabilities':
member.availabilities = value
elif line.startswith('[['):
m = re.match('\[\[(\w+)\]\]\|\[\[(\w+)\]\]\|(.*)\|', line)
if not m:
print >>self.stderr, 'Unable to parse task: "%s"' % line
continue
task = Task(m.group(1), m.group(2))
member.tasks.append(task)
if m.group(3) == '!/img/weboob/_progress.png!':
task.status = task.STATUS_PROGRESS
continue
mm = re.match('!/img/weboob/_done.png! (\d+):(\d+) (\w+)', m.group(3))
if mm and self.date:
task.status = task.STATUS_DONE
task.date = datetime(self.date.year,
self.date.month,
self.date.day,
int(mm.group(1)),
int(mm.group(2)))
task.branch = mm.group(3)
def parse_date(self, value):
try:
return datetime.strptime(value, '%Y-%m-%d')
except ValueError:
return None
def parse_time(self, value):
m = re.match('(\d+):(\d+)', value)
if not m:
return
try:
return self.date.replace(hour=int(m.group(1)),
minute=int(m.group(2)))
except ValueError:
return None
def save(self, message):
if self.winner:
finished = u'\nh3=. Event finished. Winner is "%s":/users/%d!\n' % (self.winner.name,
self.winner.id)
else:
finished = u''
s = u"""h1. %s
{{TOC}}
%s
h2. Event
%s
* *Date*: %s
* *Begin*: %s
* *End*: %s
* *Duration*: %s
* *Location*: %s
h2. Attendees
""" % (self.title,
finished,
self.description,
self.date.strftime('%Y-%m-%d') if self.date else '_Unknown_',
self.begin.strftime('%H:%M') if self.begin else '_Unknown_',
self.end.strftime('%H:%M') if self.end else '_Unknown_',
self.format_duration() or '_Unknown_',
self.location or '_Unknown_')
for member in self.members.itervalues():
if self.date:
availabilities = ''
else:
availabilities = '* *Availabilities*: %s' % member.availabilities
if member.repository is None:
repository = '_Unknown_'
elif member.repository.endswith('.git'):
repository = '"%s":git://git.symlink.me/pub/%s ("http":http://git.symlink.me/?p=%s;a=summary)'
repository = repository.replace('%s', member.repository)
else:
repository = member.repository
s += u"""h3. "%s":/users/%d
* *Repository*: %s
* *Hardware*: %s
%s
|_.Backend|_.Capabilities|_.Status|""" % (member.name,
member.id,
repository,
member.hardware,
availabilities)
for task in member.tasks:
if task.status == task.STATUS_DONE:
status = '!/img/weboob/_done.png! %02d:%02d %s' % (task.date.hour,
task.date.minute,
task.branch)
elif task.status == task.STATUS_PROGRESS:
status = '!/img/weboob/_progress.png!'
else:
status = ' '
s += u"""
|=.!/img/weboob/%s.png!:/projects/weboob/wiki/%s
[[%s]]|[[%s]]|%s|""" % (task.backend.lower(), task.backend, task.backend, task.capability, status)
s += '\n\n'
self.content.content = s
self.backend.push_content(self.content, message)
class Boobathon(ReplApplication):
APPNAME = 'boobathon'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2011 Romain Bignon'
DESCRIPTION = 'Console application to participate to a Boobathon.'
SHORT_DESCRIPTION = "participate in a Boobathon"
CAPS = CapContent
SYNOPSIS = 'Usage: %prog [-dqv] [-b backends] [-cnfs] boobathon\n'
SYNOPSIS += ' %prog [--help] [--version]'
radios = []
def __init__(self, *args, **kwargs):
ReplApplication.__init__(self, *args, **kwargs)
def main(self, argv):
if len(argv) < 2:
print >>self.stderr, 'Please give the name of the boobathon'
return 1
self.event = Event(argv[1], choice(self.weboob.backend_instances.values()))
if self.event.description is None:
if not self.ask("This event doesn't seem to exist. Do you want to create it?", default=True):
return 1
self.edit_event()
self.save_event('Event created')
return ReplApplication.main(self, [argv[0]])
def save_event(self, message):
if self.ask("Do you confirm your changes?", default=True):
self.event.save(message)
return True
return False
def edit_event(self):
self.event.title = self.ask('Enter a title', default=self.event.title)
self.event.description = self.ask('Enter a description', default=self.event.description)
self.event.date = self.ask('Enter a date (yyyy-mm-dd)',
default=self.event.date.strftime('%Y-%m-%d') if self.event.date else '',
regexp='^(\d{4}-\d{2}-\d{2})?$')
if self.event.date:
self.event.date = datetime.strptime(self.event.date, '%Y-%m-%d')
s = self.ask('Begin at (HH:MM)',
default=self.event.begin.strftime('%H:%M') if self.event.begin else '',
regexp='^(\d{2}:\d{2})?$')
if s:
h, m = s.split(':')
self.event.begin = self.event.date.replace(hour=int(h), minute=int(m))
s = self.ask('End at (HH:MM)',
default=self.event.end.strftime('%H:%M') if self.event.end else '',
regexp='^(\d{2}:\d{2})?$')
if s:
h, m = s.split(':')
self.event.end = self.event.date.replace(hour=int(h), minute=int(m))
self.event.check_time_coherence()
self.event.location = self.ask('Enter a location', default=self.event.location)
def edit_member(self, member):
if member.name is None:
firstname = self.ask('Enter your firstname')
lastname = self.ask('Enter your lastname')
member.name = '%s %s' % (firstname, lastname)
else:
member.name = self.ask('Enter your name', default=member.name)
if self.event.date is None:
member.availabilities = self.ask('Enter availabilities', default=member.availabilities)
member.repository = self.ask('Enter your repository (ex. romain/weboob.git)', default=member.repository)
member.hardware = self.ask('Enter your hardware', default=member.hardware)
def do_progress(self, line):
"""
progress
Display progress of members.
"""
self.event.load()
for member in self.event.members.itervalues():
if member.is_me and member is self.event.winner:
status = '\o/ ->'
elif member.is_me:
status = ' ->'
elif member is self.event.winner:
status = ' \o/'
else:
status = ' '
s = u' %s%20s %s|' % (status, member.shortname(), self.BOLD)
for task in member.tasks:
if task.status == task.STATUS_DONE:
s += '##'
elif task.status == task.STATUS_PROGRESS:
s += u'=>'
else:
s += ' '
s += '|%s' % self.NC
print s
print ''
now = datetime.now()
if self.event.begin > now:
d = self.event.begin - now
msg = 'The event will start in %d days, %02d:%02d:%02d'
elif self.event.end < now:
d = now - self.event.end
msg = 'The event is finished since %d days, %02d:%02d:%02d'
else:
tot = (self.event.end - self.event.begin).seconds
cur = (datetime.now() - self.event.begin).seconds
pct = cur*20/tot
progress = ''
for i in xrange(20):
if i < pct:
progress += '='
elif i == pct:
progress += '>'
else:
progress += ' '
print 'Event started: %s |%s| %s' % (self.event.begin.strftime('%H:%M'),
progress,
self.event.end.strftime('%H:%M'))
d = self.event.end - now
msg = 'The event will be finished in %d days, %02d:%02d:%02d'
print msg % (d.days, d.seconds/3600, d.seconds%3600/60, d.seconds%60)
def do_tasks(self, line):
"""
tasks
Display all tasks of members.
"""
self.event.load()
stop = False
i = -2
while not stop:
if i >= 0 and not i%2:
self.stdout.write(' #%-2d' % (i/2))
else:
self.stdout.write(' ')
if i >= 0 and i%2:
# second line of task, see if we'll stop
stop = True
for mem in self.event.members.itervalues():
if len(mem.tasks) > (i/2+1):
# there are more tasks, don't stop now
stop = False
if i == -2:
self.stdout.write(' %s%-20s%s' % (self.BOLD, mem.shortname().encode('utf-8'), self.NC))
elif i == -1:
self.stdout.write(' %s%-20s%s' % (self.BOLD, '-' * len(mem.shortname()), self.NC))
elif len(mem.tasks) <= (i/2):
self.stdout.write(' ' * (20+1))
else:
task = mem.tasks[i/2]
if task.status == task.STATUS_DONE:
status = u'#'
elif task.status == task.STATUS_PROGRESS:
if not i%2:
status = u'|' #1st line
else:
status = u'v' #2nd line
else:
status = u' '
if not i%2: #1st line
line = u'%s %s' % (status, task.backend)
else: #2nd line
line = u'%s `-%s' % (status, task.capability[3:])
self.stdout.write((u' %-20s' % line).encode('utf-8'))
self.stdout.write('\n')
i += 1
def complete_close(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
self.event.load()
return [member.name for member in self.event.members.itervalues()]
def do_close(self, name):
"""
close WINNER
Close the event and set the winner.
"""
self.event.load()
for member in self.event.members.itervalues():
if member.name == name:
self.event.winner = member
if self.save_event('Close event'):
print 'Event is now closed. Winner is %s!' % self.event.winner.name
return
print >>self.stderr, '"%s" not found' % name
return 3
def complete_edit(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return ['event', 'me']
def do_edit(self, line):
"""
edit [event | me]
Edit information about you or about event.
"""
if not line:
print >>self.stderr, 'Syntax: edit [event | me]'
return 2
self.event.load()
if line == 'event':
self.edit_event()
self.save_event('Event edited')
elif line == 'me':
mem = self.event.get_me()
if not mem:
print >>self.stderr, 'You haven\'t joined the event.'
return 1
self.edit_member(mem)
self.save_event('Member edited')
else:
print >>self.stderr, 'Unable to edit "%s"' % line
return 1
def do_info(self, line):
"""
info
Display information about this event.
"""
self.event.load()
print self.event.title
print '-' * len(self.event.title)
print self.event.description
print ''
print 'Date:', self.event.date.strftime('%Y-%m-%d') if self.event.date else 'Unknown'
print 'Begin:', self.event.begin.strftime('%H:%M') if self.event.begin else 'Unknown'
print 'End:', self.event.end.strftime('%H:%M') if self.event.end else 'Unknown'
print 'Duration:', self.event.format_duration() or 'Unknown'
print 'Location:', self.event.location or 'Unknown'
print ''
print 'There are %d members, use the "members" command to list them' % len(self.event.members)
if self.event.get_me() is None:
print 'To join this event, use the command "join".'
def do_members(self, line):
"""
members
Display members information.
"""
self.event.load()
for member in self.event.members.itervalues():
print member.name
print '-' * len(member.name)
print 'Repository:', member.repository
if self.event.date is None:
print 'Availabilities:', member.availabilities
print 'Hardware:', member.hardware
accompl = 0
for task in member.tasks:
if task.status == task.STATUS_DONE:
accompl += 1
print '%d tasks (%d accomplished)' % (len(member.tasks), accompl)
if member is self.event.winner:
print '=== %s is the winner!' % member.name
print ''
print 'Use the "tasks" command to display all tasks'
def do_join(self, line):
"""
join
Join this event.
"""
self.event.load()
if self.event.backend.browser.get_userid() in self.event.members:
print >>self.stderr, 'You have already joined this event.'
return 1
if self.event.is_closed():
print >>self.stderr, "Boobathon is closed."
return 1
m = Member(self.event.backend.browser.get_userid(), None)
self.edit_member(m)
self.event.members[m.id] = m
self.save_event('Joined the event')
def do_leave(self, line):
"""
leave
Leave this event.
"""
self.event.load()
if self.event.currently_in_event():
print >>self.stderr, 'Unable to leave during the event, loser!'
return 1
if self.event.is_closed():
print >>self.stderr, "Boobathon is closed."
return 1
try:
self.event.members.pop(self.event.backend.browser.get_userid())
except KeyError:
print >>self.stderr, "You have not joined this event."
return 1
else:
self.save_event('Left the event')
def do_remtask(self, line):
"""
remtask TASK_ID
Remove a task.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print >>self.stderr, "You have not joined this event."
return 1
if self.event.is_closed():
print >>self.stderr, "Boobathon is closed."
return 1
try:
task_id = int(line)
except ValueError:
print >>self.stderr, 'The task ID should be a number'
return 2
try:
task = mem.tasks.pop(task_id)
except IndexError:
print >>self.stderr, 'Unable to find task #%d' % task_id
return 1
else:
print 'Removing task #%d (%s,%s).' % (task_id, task.backend, task.capability)
self.save_event('Remove task')
def do_addtask(self, line):
"""
addtask BACKEND CAPABILITY
Add a new task.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print >>self.stderr, "You have not joined this event."
return 1
if self.event.is_closed():
print >>self.stderr, "Boobathon is closed."
return 1
backend, capability = self.parse_command_args(line, 2, 2)
if not backend[0].isupper():
print >>self.stderr, 'The backend name "%s" needs to start with a capital.' % backend
return 2
if not capability.startswith('Cap') or not capability[3].isupper():
print >>self.stderr, '"%s" is not a proper capability name (must start with Cap).' % capability
return 2
for task in mem.tasks:
if (task.backend,task.capability) == (backend,capability):
print >>self.stderr, "A task already exists for that."
return 1
task = Task(backend, capability)
mem.tasks.append(task)
self.save_event('New task')
def do_start(self, line):
"""
start [TASK_ID]
Start a task. If you don't give a task ID, the first available
task will be taken.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print >>self.stderr, "You have not joined this event."
return 1
if len(mem.tasks) == 0:
print >>self.stderr, "You don't have any task to do."
return 1
if not self.event.currently_in_event():
print >>self.stderr, "You can't start a task, we are not in event."
return 1
if line.isdigit():
task_id = int(line)
else:
task_id = -1
last_done = -1
for i, task in enumerate(mem.tasks):
if task.status == task.STATUS_DONE:
last_done = i
elif task.status == task.STATUS_PROGRESS:
task.status = task.STATUS_NONE
print 'Task #%s (%s,%s) canceled.' % (i, task.backend, task.capability)
if (i == task_id or task_id < 0) and task.status == task.STATUS_NONE:
break
else:
print >>self.stderr, 'Task not found.'
return 3
if task.status == task.STATUS_DONE:
print >>self.stderr, 'Task is already done.'
return 1
task.status = task.STATUS_PROGRESS
mem.tasks.remove(task)
mem.tasks.insert(last_done + 1, task)
self.save_event('Started a task')
def do_done(self, line):
"""
done
Set the current task as done.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print >>self.stderr, "You have not joined this event."
return 1
if self.event.is_closed():
print >>self.stderr, "Boobathon is closed."
return 1
for i, task in enumerate(mem.tasks):
if task.status == task.STATUS_PROGRESS:
print 'Task (%s,%s) done! (%d%%)' % (task.backend, task.capability, (i+1)*100/len(mem.tasks))
if self.event.currently_in_event():
task.status = task.STATUS_DONE
task.date = datetime.now()
task.branch = self.ask('Enter name of branch')
self.save_event('Task accomplished')
else:
task.status = task.STATUS_NONE
print >>self.stderr, 'Oops, you are out of event. Canceling the task...'
self.save_event('Cancel task')
return 1
return
print >>self.stderr, "There isn't any task in progress."
return 1
def do_cancel(self, line):
"""
cancel
Cancel the current task.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print >>self.stderr, "You have not joined this event."
return 1
if self.event.is_closed():
print >>self.stderr, "Boobathon is closed."
return 1
for task in mem.tasks:
if task.status == task.STATUS_PROGRESS:
print 'Task (%s,%s) canceled.' % (task.backend, task.capability)
task.status = task.STATUS_NONE
self.save_event('Cancel task')
return
print >>self.stderr, "There isn't any task in progress."
return 1
def load_default_backends(self):
"""
Overload a Application method.
"""
for instance_name, backend_name, params in self.weboob.backends_config.iter_backends():
if backend_name != 'redmine':
continue
v = urlsplit(params['url'])
if v.netloc == 'symlink.me':
self.load_backends(names=[instance_name])
return
if not self.check_loaded_backends({'url': 'https://symlink.me'}):
print "Ok, so leave now, fag."
sys.exit(0)
def is_module_loadable(self, module):
"""
Overload a ConsoleApplication method.
"""
return module.name == 'redmine'
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobcoming/ 0000775 0000000 0000000 00000000000 12411220504 0026445 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobcoming/__init__.py 0000664 0000000 0000000 00000001424 12411220504 0030557 0 ustar 00root root 0000000 0000000
# -*- coding: utf-8 -*-
# Copyright(C) 2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobcoming import Boobcoming
__all__ = ['Boobcoming']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobcoming/boobcoming.py 0000664 0000000 0000000 00000030247 12411220504 0031143 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from datetime import time, datetime
from weboob.tools.date import parse_date
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.capabilities.base import empty
from weboob.capabilities.calendar import CapCalendarEvent, Query, CATEGORIES, BaseCalendarEvent
from weboob.tools.application.repl import ReplApplication, defaultcount
__all__ = ['Boobcoming']
class UpcomingSimpleFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'category', 'summary')
def format_obj(self, obj, alias):
return u'%s - %s - %s - %s' % (obj.backend, obj.category, obj.start_date.strftime('%H:%M'), obj.summary)
class ICalFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'status')
def start_format(self, **kwargs):
result = u'BEGIN:VCALENDAR\n'
result += u'VERSION:2.0\n'
result += u'PRODID:-//hacksw/handcal//NONSGML v1.0//EN\n'
self.output(result)
def format_obj(self, obj, alias):
result = u'BEGIN:VEVENT\n'
result += u'DTSTART:%s\n' % obj.start_date.strftime("%Y%m%dT%H%M%SZ")
result += u'DTEND:%s\n' % obj.end_date.strftime("%Y%m%dT%H%M%SZ")
result += u'SUMMARY:%s\n' % obj.summary
result += u'UID:%s\n' % obj.id
result += u'STATUS:%s\n' % obj.status
location = ''
if hasattr(obj, 'location') and not empty(obj.location):
location += obj.location + ' '
if hasattr(obj, 'city') and not empty(obj.city):
location += obj.city + ' '
if not empty(location):
result += u'LOCATION:%s\n' % location
if hasattr(obj, 'categories') and not empty(obj.categories):
result += u'CATEGORIES:%s\n' % obj.categories
if hasattr(obj, 'description') and not empty(obj.description):
result += u'DESCRIPTION:%s\n' % obj.description.replace('\r\n', '\\n') \
.replace(',', '\,')
if hasattr(obj, 'transp') and not empty(obj.transp):
result += u'TRANSP:%s\n' % obj.transp
if hasattr(obj, 'sequence') and not empty(obj.sequence):
result += u'SEQUENCE:%s\n' % obj.sequence
if hasattr(obj, 'url') and not empty(obj.url):
result += u'URL:%s\n' % obj.url
result += u'END:VEVENT\n'
return result
def flush(self, **kwargs):
self.output(u'END:VCALENDAR')
class UpcomingListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'category')
def get_title(self, obj):
return ' %s - %s ' % (obj.category, obj.summary)
def get_description(self, obj):
result = u''
result += u'\tDate: %s\n' % obj.start_date.strftime('%A %d %B %Y')
result += u'\tHour: %s - %s \n' % (obj.start_date.strftime('%H:%M'), obj.end_date.strftime('%H:%M'))
return result.strip('\n\t')
class UpcomingFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'category')
def format_obj(self, obj, alias):
result = u'%s%s - %s%s\n' % (self.BOLD, obj.category, obj.summary, self.NC)
result += u'Date: %s\n' % obj.start_date.strftime('%A %d %B %Y')
result += u'Hour: %s - %s\n' % (obj.start_date.strftime('%H:%M'), obj.end_date.strftime('%H:%M'))
if hasattr(obj, 'location') and not empty(obj.location):
result += u'Location: %s\n' % obj.location
if hasattr(obj, 'city') and not empty(obj.city):
result += u'City: %s\n' % obj.city
if hasattr(obj, 'event_planner') and not empty(obj.event_planner):
result += u'Event planner: %s\n' % obj.event_planner
if hasattr(obj, 'booked_entries') and not empty(obj.booked_entries) and \
hasattr(obj, 'max_entries') and not empty(obj.max_entries):
result += u'Entry: %s/%s \n' % (obj.booked_entries, obj.max_entries)
elif hasattr(obj, 'booked_entries') and not empty(obj.booked_entries):
result += u'Entry: %s \n' % (obj.booked_entries)
elif hasattr(obj, 'max_entries') and not empty(obj.max_entries):
result += u'Max entries: %s \n' % (obj.max_entries)
if hasattr(obj, 'description') and not empty(obj.description):
result += u'Description:\n %s\n\n' % obj.description
if hasattr(obj, 'price') and not empty(obj.price):
result += u'Price: %i\n' % obj.price
if hasattr(obj, 'url') and not empty(obj.url):
result += u'url: %s\n' % obj.url
return result
class Boobcoming(ReplApplication):
APPNAME = 'boobcoming'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2012 Bezleputh'
DESCRIPTION = "Console application to see upcoming events."
SHORT_DESCRIPTION = "see upcoming events"
CAPS = CapCalendarEvent
EXTRA_FORMATTERS = {'upcoming_list': UpcomingListFormatter,
'upcoming': UpcomingFormatter,
'simple_upcoming': UpcomingSimpleFormatter,
'ical_formatter': ICalFormatter,
}
COMMANDS_FORMATTERS = {'list': 'upcoming_list',
'search': 'upcoming_list',
'ls': 'upcoming_list',
'info': 'upcoming',
'export': 'ical_formatter'
}
def comp_object(self, obj1, obj2):
if isinstance(obj1, BaseCalendarEvent) and isinstance(obj2, BaseCalendarEvent):
if obj1.start_date == obj2.start_date:
return 0
if obj1.start_date > obj2.start_date:
return 1
return -1
else:
return super(Boobcoming, self).comp_object(obj1, obj2)
@defaultcount(10)
def do_search(self, line):
"""
search
search for an event. Parameters interactively asked
"""
query = Query()
r = 'notempty'
while r != '':
for category in CATEGORIES.values:
print ' %s%2d)%s [%s] %s' % (self.BOLD,
CATEGORIES.index[category] + 1,
self.NC,
'x' if category in query.categories else ' ', category)
r = self.ask(' Select category (or empty to stop)', regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(CATEGORIES.values):
continue
value = CATEGORIES.values[r - 1]
if value in query.categories:
query.categories.remove(value)
else:
query.categories.append(value)
if query.categories and len(query.categories) > 0:
query.city = self.ask('Enter a city', default='')
start_date = self.ask_date('Enter a start date', default='today')
end_date = self.ask_date('Enter a end date', default='')
if end_date:
if end_date == start_date:
end_date = datetime.combine(start_date, time.max)
else:
end_date = datetime.combine(end_date, time.max)
query.start_date = datetime.combine(start_date, time.min)
query.end_date = end_date
self.change_path([u'events'])
self.start_format()
for backend, event in self.do('search_events', query):
if event:
self.cached_format(event)
def ask_date(self, txt, default=''):
r = self.ask(txt, default=default)
return parse_date(r)
@defaultcount(10)
def do_list(self, line):
"""
list [PATTERN]
List upcoming events, pattern can be an english or french week day, 'today' or a date (dd/mm/yy[yy])
"""
self.change_path([u'events'])
if line:
_date = parse_date(line)
if not _date:
print >>self.stderr, 'Invalid argument: %s' % self.get_command_help('list')
return 2
date_from = datetime.combine(_date, time.min)
date_to = datetime.combine(_date, time.max)
else:
date_from = datetime.now()
date_to = None
for backend, event in self.do('list_events', date_from, date_to):
self.cached_format(event)
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about an event.
"""
if not _id:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True)
return 2
event = self.get_object(_id, 'get_event')
if not event:
print >>self.stderr, 'Upcoming event not found: %s' % _id
return 3
self.start_format()
self.format(event)
def do_export(self, line):
"""
export FILENAME [ID1 ID2 ID3 ...]
ID is the identifier of the event. If no ID every events are exported
FILENAME is where to write the file. If FILENAME is '-', the file is written to stdout.
Export event in ICALENDAR format
"""
if not line:
print >>self.stderr, 'This command takes at leat one argument: %s' % self.get_command_help('export')
return 2
_file, args = self.parse_command_args(line, 2, req_n=1)
if not _file == "-":
dest = self.check_file_ext(_file)
self.formatter.outfile = dest
l = self.retrieve_events(args)
self.formatter.start_format()
for item in l:
self.format(item)
def retrieve_events(self, args):
l = []
if not args:
_ids = []
for backend, event in self.do('list_events', datetime.now(), None):
_ids.append(event.id)
else:
_ids = args.strip().split(' ')
for _id in _ids:
event = self.get_object(_id, 'get_event')
if not event:
print >>self.stderr, 'Upcoming event not found: %s' % _id
return 3
l.append(event)
return l
def check_file_ext(self, _file):
splitted_file = _file.split('.')
if splitted_file[-1] != 'ics':
return "%s.ics" % _file
else:
return _file
def do_attends(self, line):
"""
attends ID1 [ID2 ID3 ...]
Register as participant of an event.
ID is the identifier of the event.
"""
if not line:
print >>self.stderr, 'This command takes at leat one argument: %s' % self.get_command_help('attends')
return 2
args = self.parse_command_args(line, 1, req_n=1)
l = self.retrieve_events(args[0])
for event in l:
self.do('attends_event', event, True)
def do_unattends(self, line):
"""
unattends ID1 [ID2 ID3 ...]
Unregister you participation for an event.
ID is the identifier of the event.
"""
if not line:
print >>self.stderr, 'This command takes at leat one argument: %s' % self.get_command_help('unattends')
return 2
args = self.parse_command_args(line, 1, req_n=1)
l = self.retrieve_events(args[0])
for event in l:
self.do('attends_event', event, False)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobill/ 0000775 0000000 0000000 00000000000 12411220504 0025751 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobill/__init__.py 0000664 0000000 0000000 00000001530 12411220504 0030061 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai
# Copyright(C) 2011 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobill import Boobill
__all__ = ['Boobill']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobill/boobill.py 0000664 0000000 0000000 00000017765 12411220504 0027765 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012-2013 Florent Fourcot
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from decimal import Decimal
from weboob.capabilities.bill import CapBill, Detail, Subscription
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import PrettyFormatter
from weboob.tools.application.base import MoreResultsAvailable
from weboob.core import CallErrors
__all__ = ['Boobill']
class SubscriptionsFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'label')
def get_title(self, obj):
if obj.renewdate:
return u"%s - %s" % (obj.label, obj.renewdate.strftime('%d/%m/%y'))
return obj.label
class Boobill(ReplApplication):
APPNAME = 'boobill'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2012 Florent Fourcot'
DESCRIPTION = 'Console application allowing to get and download bills.'
SHORT_DESCRIPTION = "get and download bills"
CAPS = CapBill
COLLECTION_OBJECTS = (Subscription, )
EXTRA_FORMATTERS = {'subscriptions': SubscriptionsFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'subscriptions': 'subscriptions',
'ls': 'subscriptions',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def exec_method(self, id, method):
l = []
id, backend_name = self.parse_id(id)
if not id:
for subscrib in self.get_object_list('iter_subscription'):
l.append((subscrib.id, subscrib.backend))
else:
l.append((id, backend_name))
more_results = []
not_implemented = []
self.start_format()
for id, backend in l:
names = (backend,) if backend is not None else None
try:
for backend, result in self.do(method, id, backends=names):
self.format(result)
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, MoreResultsAvailable):
more_results.append(id + u'@' + backend.name)
elif isinstance(error, NotImplementedError):
if backend not in not_implemented:
not_implemented.append(backend)
else:
self.bcall_error_handler(backend, error, backtrace)
if len(more_results) > 0:
print >>self.stderr, 'Hint: There are more results available for %s (use option -n or count command)' % (', '.join(more_results))
for backend in not_implemented:
print >>self.stderr, u'Error(%s): This feature is not supported yet by this backend.' % backend.name
def do_subscriptions(self, line):
"""
subscriptions
List all subscriptions.
"""
self.start_format()
for subscription in self.get_object_list('iter_subscription'):
self.format(subscription)
def do_details(self, id):
"""
details [ID]
Get details of subscriptions.
If no ID given, display all details of all backends.
"""
l = []
id, backend_name = self.parse_id(id)
if not id:
for subscrib in self.get_object_list('iter_subscription'):
l.append((subscrib.id, subscrib.backend))
else:
l.append((id, backend_name))
for id, backend in l:
names = (backend,) if backend is not None else None
# XXX: should be generated by backend? -Flo
# XXX: no, but you should do it in a specific formatter -romain
# TODO: do it, and use exec_method here. Code is obsolete
mysum = Detail()
mysum.label = u"Sum"
mysum.infos = u"Generated by boobill"
mysum.price = Decimal("0.")
self.start_format()
for backend, detail in self.do('get_details', id, backends=names):
self.format(detail)
mysum.price = detail.price + mysum.price
self.format(mysum)
def do_balance(self, id):
"""
balance [ID]
Get balance of subscriptions.
If no ID given, display balance of all backends.
"""
self.exec_method(id, 'get_balance')
@defaultcount(10)
def do_history(self, id):
"""
history [ID]
Get the history of subscriptions.
If no ID given, display histories of all backends.
"""
self.exec_method(id, 'iter_bills_history')
@defaultcount(10)
def do_bills(self, id):
"""
bills [ID]
Get the list of bills documents for subscriptions.
If no ID given, display bills of all backends
"""
self.exec_method(id, 'iter_bills')
def do_download(self, line):
"""
download [ID | all] [FILENAME]
download ID [FILENAME]
download the bill
id is the identifier of the bill (hint: try bills command)
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
download all [ID]
You can use special word "all" and download all bills of
subscription identified by ID.
If Id not given, download bills of all subscriptions.
"""
id, dest = self.parse_command_args(line, 2, 1)
id, backend_name = self.parse_id(id)
if not id:
print >>self.stderr, 'Error: please give a bill ID (hint: use bills command)'
return 2
names = (backend_name,) if backend_name is not None else None
# Special keywords, download all bills of all subscriptions
if id == "all":
if dest is None:
for backend, subscription in self.do('iter_subscription', backends=names):
self.download_all(subscription.id, names)
return
else:
self.download_all(dest, names)
return
if dest is None:
for backend, bill in self.do('get_bill', id, backends=names):
dest = id + "." + bill.format
for backend, buf in self.do('download_bill', id, backends=names):
if buf:
if dest == "-":
print buf
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print >>self.stderr, 'Unable to write bill in "%s": %s' % (dest, e)
return 1
return
def download_all(self, id, names):
id, backend_name = self.parse_id(id)
for backend, bill in self.do('iter_bills', id, backends=names):
dest = bill.id + "." + bill.format
for backend2, buf in self.do('download_bill', bill.id, backends=names):
if buf:
if dest == "-":
print buf
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print >>self.stderr, 'Unable to write bill in "%s": %s' % (dest, e)
return 1
return
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/booblyrics/ 0000775 0000000 0000000 00000000000 12411220504 0026476 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/booblyrics/__init__.py 0000664 0000000 0000000 00000001432 12411220504 0030607 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .booblyrics import Booblyrics
__all__ = ['Booblyrics']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/booblyrics/booblyrics.py 0000664 0000000 0000000 00000006773 12411220504 0031234 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.lyrics import CapLyrics
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Booblyrics', 'LyricsGetFormatter', 'LyricsListFormatter']
class LyricsGetFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'title', 'artist', 'content')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.title, self.NC)
result += 'ID: %s\n' % obj.fullid
result += 'Title: %s\n' % obj.title
result += 'Artist: %s\n' % obj.artist
result += '\n%sContent%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.content
return result
class LyricsListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'artist')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
artist = u''
if not empty(obj.artist):
artist = obj.artist
return '%s' % artist
class Booblyrics(ReplApplication):
APPNAME = 'booblyrics'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2013 Julien Veyssier'
DESCRIPTION = "Console application allowing to search for song lyrics on various websites."
SHORT_DESCRIPTION = "search and display song lyrics"
CAPS = CapLyrics
EXTRA_FORMATTERS = {'lyrics_list': LyricsListFormatter,
'lyrics_get': LyricsGetFormatter,
}
COMMANDS_FORMATTERS = {'search': 'lyrics_list',
'get': 'lyrics_get',
}
SEARCH_CRITERIAS = ['artist', 'song']
def complete_get(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_get(self, id):
"""
get ID
Display lyrics of the song.
"""
songlyrics = self.get_object(id, 'get_lyrics')
if not songlyrics:
print >>self.stderr, 'Song lyrics not found: %s' % id
return 3
self.start_format()
self.format(songlyrics)
def complete_search(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self.SEARCH_CRITERIAS
@defaultcount(10)
def do_search(self, line):
"""
search [artist | song] [PATTERN]
Search lyrics by artist name or by song title.
"""
criteria, pattern = self.parse_command_args(line, 2, 2)
self.change_path([u'search'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, songlyrics in self.do('iter_lyrics', criteria, pattern):
self.cached_format(songlyrics)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobmsg/ 0000775 0000000 0000000 00000000000 12411220504 0025757 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobmsg/__init__.py 0000664 0000000 0000000 00000001426 12411220504 0030073 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobmsg import Boobmsg
__all__ = ['Boobmsg']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobmsg/boobmsg.py 0000664 0000000 0000000 00000041744 12411220504 0027773 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import datetime
import hashlib
from tempfile import NamedTemporaryFile
from lxml import etree
from weboob.core import CallErrors
from weboob.capabilities.messages import CapMessages, Message, Thread
from weboob.capabilities.account import CapAccount
from weboob.capabilities.contact import CapContact
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter
from weboob.tools.html import html2text
__all__ = ['Boobmsg']
class AtomFormatter(IFormatter):
MANDATORY_FIELDS = ('title', 'date', 'sender', 'content')
def start_format(self, **kwargs):
self.output(u'\n')
self.output(u'Atom feed by Weboob') # TODO : get backend name
self.output(u'%s' % datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"))
m = hashlib.md5()
m.update(datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"))
self.output(u'urn:md5:%s' % m.hexdigest())
def format_obj(self, obj, alias):
elem = etree.Element('entry')
title = etree.Element('title')
title.text = obj.title
elem.append(title)
id = etree.Element('id')
m = hashlib.md5()
m.update(obj.content.encode('utf8', 'ascii'))
id.text = "urn:md5:%s" % m.hexdigest()
elem.append(id)
link = etree.Element('link')
link.attrib["href"] = obj.thread.id
link.attrib["title"] = obj.title
link.attrib["type"] = "text/html"
elem.append(link)
author = etree.Element('author')
name = etree.Element('name')
if obj.sender:
name.text = obj.sender
else:
name.text = obj.backend
author.append(name)
elem.append(author)
date = etree.Element('updated')
date.text = obj.date.strftime("%Y-%m-%dT%H:%M:%SZ")
elem.append(date)
content = etree.Element('content')
content.text = obj.content
content.attrib["type"] = "html"
elem.append(content)
return etree.tostring(elem, pretty_print=True)
def flush(self):
self.output(u'')
class XHtmlFormatter(IFormatter):
MANDATORY_FIELDS = ('title', 'date', 'sender', 'signature', 'content')
def format_obj(self, obj, alias):
result = "
\n"
result += "
%s
" % (obj.title)
result += "
"
result += "
Date
%s
" % (obj.date.strftime('%Y-%m-%d %H:%M'))
result += "
Sender
%s
" % (obj.sender)
result += "
Signature
%s
" % (obj.signature)
result += "
"
result += "
%s
" % (obj.content)
result += "
\n"
return result
class MessageFormatter(IFormatter):
MANDATORY_FIELDS = ('title', 'date', 'sender', 'signature', 'content')
def format_obj(self, obj, alias):
result = u'%sTitle:%s %s\n' % (self.BOLD,
self.NC, obj.title)
result += u'%sDate:%s %s\n' % (self.BOLD,
self.NC, obj.date.strftime('%Y-%m-%d %H:%M'))
result += u'%sFrom:%s %s\n' % (self.BOLD,
self.NC, obj.sender)
if hasattr(obj, 'receivers') and obj.receivers:
result += u'%sTo:%s %s\n' % (self.BOLD,
self.NC,
', '.join(obj.receivers))
if obj.flags & Message.IS_HTML:
content = html2text(obj.content)
else:
content = obj.content
result += '\n%s' % content
if obj.signature:
if obj.flags & Message.IS_HTML:
signature = html2text(obj.signature)
else:
signature = obj.signature
result += '\n-- \n%s' % signature
return result
class MessagesListFormatter(IFormatter):
MANDATORY_FIELDS = ()
count = 0
_list_messages = False
def flush(self):
self.count = 0
def format_obj(self, obj, alias):
if not self._list_messages:
return self.format_dict_thread(obj, alias)
else:
return self.format_dict_messages(obj, alias)
def format_dict_thread(self, obj, alias):
self.count += 1
if self.interactive:
result = u'%s* (%d) %s (%s)%s' % (self.BOLD,
self.count,
obj.title, obj.backend,
self.NC)
else:
result = u'%s* (%s) %s%s' % (self.BOLD, obj.id,
obj.title,
self.NC)
if obj.date:
result += u'\n %s' % obj.date
return result
def format_dict_messages(self, obj, alias):
if obj.flags == Thread.IS_THREADS:
depth = 0
else:
depth = -1
result = self.format_message(obj.backend, obj.root, depth)
return result
def format_message(self, backend, message, depth=0):
if not message:
return u''
self.count += 1
flags = '['
if message.flags & message.IS_UNREAD:
flags += 'N'
else:
flags += '-'
if message.flags & message.IS_NOT_RECEIVED:
flags += 'U'
elif message.flags & message.IS_RECEIVED:
flags += 'R'
else:
flags += '-'
flags += ']'
if self.interactive:
result = u'%s%s* (%d)%s %s <%s> %s (%s)\n' % (depth * ' ',
self.BOLD,
self.count,
self.NC,
flags,
message.sender,
message.title,
backend)
else:
result = u'%s%s* (%s.%s@%s)%s %s <%s> %s\n' % (depth * ' ',
self.BOLD,
message.thread.id,
message.id,
backend,
self.NC,
flags,
message.sender,
message.title)
if message.children:
if depth >= 0:
depth += 1
for m in message.children:
result += self.format_message(backend, m, depth)
return result
class ProfileFormatter(IFormatter):
def flush(self):
pass
def format_obj(self, obj, alias=None):
return obj.get_text()
class Boobmsg(ReplApplication):
APPNAME = 'boobmsg'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Christophe Benz'
DESCRIPTION = "Console application allowing to send messages on various websites and " \
"to display message threads and contents."
SHORT_DESCRIPTION = "send and receive message threads"
CAPS = CapMessages
EXTRA_FORMATTERS = {'msglist': MessagesListFormatter,
'msg': MessageFormatter,
'xhtml': XHtmlFormatter,
'atom': AtomFormatter,
'profile' : ProfileFormatter,
}
COMMANDS_FORMATTERS = {'list': 'msglist',
'show': 'msg',
'export_thread': 'msg',
'export_all': 'msg',
'ls': 'msglist',
'profile': 'profile',
}
def add_application_options(self, group):
group.add_option('-E', '--accept-empty', action='store_true',
help='Send messages with an empty body.')
group.add_option('-t', '--title', action='store',
help='For the "post" command, set a title to message',
type='string', dest='title')
def load_default_backends(self):
self.load_backends(CapMessages, storage=self.create_storage())
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def do_status(self, line):
"""
status
Display status information about a backend.
"""
if len(line) > 0:
backend_name = line
else:
backend_name = None
results = {}
for backend, field in self.do('get_account_status',
backends=backend_name,
caps=CapAccount):
if backend.name in results:
results[backend.name].append(field)
else:
results[backend.name] = [field]
for name, fields in results.iteritems():
print ':: %s ::' % name
for f in fields:
if f.flags & f.FIELD_HTML:
value = html2text(f.value)
else:
value = f.value
print '%s: %s' % (f.label, value)
print ''
def do_post(self, line):
"""
post RECEIVER@BACKEND[,RECEIVER@BACKEND[...]] [TEXT]
Post a message to the specified receivers.
Multiple receivers are separated by a comma.
If no text is supplied on command line, the content of message is read on stdin.
"""
receivers, text = self.parse_command_args(line, 2, 1)
if text is None:
text = self.acquire_input()
if not self.options.accept_empty and not text.strip():
self.logger.warning(u'The message body is empty, use option --accept_empty to send empty messages')
return
for receiver in receivers.strip().split(','):
receiver, backend_name = self.parse_id(receiver.strip(),
unique_backend=True)
if not backend_name and len(self.enabled_backends) > 1:
self.logger.warning(u'No backend specified for receiver "%s": message will be sent with all the '
'enabled backends (%s)' % (receiver,
','.join(backend.name for backend in self.enabled_backends)))
if '.' in receiver:
# It's a reply
thread_id, parent_id = receiver.rsplit('.', 1)
else:
# It's an original message
thread_id = receiver
parent_id = None
try:
thread_id = self.threads[int(thread_id) - 1].id
except (IndexError,ValueError):
pass
thread = Thread(thread_id)
message = Message(thread,
0,
title=self.options.title,
parent=Message(thread, parent_id) if parent_id else None,
content=text)
try:
self.do('post_message', message, backends=backend_name).wait()
except CallErrors as errors:
self.bcall_errors_handler(errors)
else:
if self.interactive:
print 'Message sent sucessfully to %s' % receiver
threads = []
messages = []
@defaultcount(10)
def do_list(self, arg):
"""
list
Display all threads.
"""
if len(arg) > 0:
try:
thread = self.threads[int(arg) - 1]
except (IndexError, ValueError):
id, backend_name = self.parse_id(arg)
else:
id = thread.id
backend_name = thread.backend
self.messages = []
cmd = self.do('get_thread', id, backends=backend_name)
self.formatter._list_messages = True
else:
self.threads = []
cmd = self.do('iter_threads')
self.formatter._list_messages = False
self.start_format()
for backend, thread in cmd:
if not thread:
continue
if len(arg) > 0:
for m in thread.iter_all_messages():
if not m.backend:
m.backend = thread.backend
self.messages.append(m)
else:
self.threads.append(thread)
self.format(thread)
def do_export_all(self, arg):
"""
export_all
Export All threads
"""
def func(backend):
for thread in backend.iter_threads():
if not thread:
continue
t = backend.fillobj(thread, None)
for msg in t.iter_all_messages():
yield msg
self.start_format()
for backend, msg in self.do(func):
self.format(msg)
def do_export_thread(self, arg):
"""
export_thread ID
Export the thread identified by ID
"""
_id, backend_name = self.parse_id(arg)
cmd = self.do('get_thread', _id, backends=backend_name)
self.start_format()
for backend, thread in cmd:
if thread is not None :
for msg in thread.iter_all_messages():
self.format(msg)
def do_show(self, arg):
"""
show MESSAGE
Read a message
"""
message = None
if len(arg) == 0:
print >>self.stderr, 'Please give a message ID.'
return 2
try:
message = self.messages[int(arg) - 1]
except (IndexError, ValueError):
id, backend_name = self.parse_id(arg)
cmd = self.do('get_thread', id, backends=backend_name)
for backend, thread in cmd:
if thread is not None:
message = thread.root
if message is not None:
self.start_format()
self.format(message)
self.weboob.do('set_message_read', message, backends=message.backend)
return
else:
print >>self.stderr, 'Message not found'
return 3
def do_profile(self, id):
"""
profile ID
Display a profile
"""
_id, backend_name = self.parse_id(id, unique_backend=True)
found = 0
for backend, contact in self.do('get_contact', _id, backends=backend_name, caps=CapContact):
if contact:
self.format(contact)
found = 1
if not found:
self.logger.error(u'Profile not found')
def do_photos(self, id):
"""
photos ID
Display photos of a profile
"""
photo_cmd = self.config.get('photo_viewer')
if photo_cmd is None:
print >>self.stderr, "Configuration error: photo_viewer is undefined"
return
_id, backend_name = self.parse_id(id, unique_backend=True)
found = 0
for backend, contact in self.do('get_contact', _id, backends=backend_name):
if contact:
# Write photo to temporary files
tmp_files = []
for photo in contact.photos.values():
suffix = '.jpg'
if '.' in photo.url.split('/')[-1]:
suffix = '.%s' % photo.url.split('/')[-1].split('.')[-1]
f = NamedTemporaryFile(suffix=suffix)
photo = backend.fillobj(photo, 'data')
f.write(photo.data)
tmp_files.append(f)
os.system(photo_cmd % ' '.join([file.name for file in tmp_files]))
found = 1
if not found:
self.logger.error(u'Profile not found')
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobooks/ 0000775 0000000 0000000 00000000000 12411220504 0026144 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobooks/__init__.py 0000664 0000000 0000000 00000001431 12411220504 0030254 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Jérémy Monnet
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobooks import Boobooks
__all__ = ['Boobooks']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobooks/boobooks.py 0000664 0000000 0000000 00000004524 12411220504 0030340 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2009-2012 Jeremy Monnet
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.library import CapBook, Book
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Boobooks']
class RentedListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'date', 'author', 'name', 'late')
RED = '[1;31m'
def get_title(self, obj):
s = u'%s — %s (%s)' % (obj.author, obj.name, obj.date)
if obj.late:
s += u' %sLATE!%s' % (self.RED, self.NC)
return s
class Boobooks(ReplApplication):
APPNAME = 'boobooks'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2012 Jeremy Monnet'
CAPS = CapBook
DESCRIPTION = "Console application allowing to list your books rented or booked at the library, " \
"book and search new ones, get your booking history (if available)."
SHORT_DESCRIPTION = "manage rented books"
EXTRA_FORMATTERS = {'rented_list': RentedListFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'ls': 'rented_list',
'list': 'rented_list',
}
COLLECTION_OBJECTS = (Book, )
def do_renew(self, id):
"""
renew ID
Renew a book
"""
id, backend_name = self.parse_id(id)
if not id:
print >>self.stderr, 'Error: please give a book ID (hint: use ls command)'
return 2
names = (backend_name,) if backend_name is not None else None
for backend, renew in self.do('renew_book', id, backends=names):
self.format(renew)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobsize/ 0000775 0000000 0000000 00000000000 12411220504 0026143 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobsize/__init__.py 0000664 0000000 0000000 00000001425 12411220504 0030256 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Florent Fourcot
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobsize import Boobsize
__all__ = ['Boobsize']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobsize/boobsize.py 0000664 0000000 0000000 00000015653 12411220504 0030343 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Florent Fourcot
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.base import empty
from weboob.capabilities.gauge import CapGauge, SensorNotFound
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['Boobsize']
class GaugeFormatter(IFormatter):
MANDATORY_FIELDS = ('name', 'object', 'sensors')
DISPLAYED_FIELDS = ('city', )
def start_format(self, **kwargs):
# Name = 27 Object = 10 City = 10 Sensors = 33
self.output(' Name and ID Object City Sensors ')
self.output('----------------------------+----------+----------+---------------------------------')
def format_obj(self, obj, alias):
name = obj.name
city = u""
if not empty(obj.city):
city = obj.city
if not obj.sensors or (len(obj.sensors) == 0):
result = u' %s %s %s \n' %\
(self.colored('%-27s' % name[:27], 'red'),
self.colored('%-10s' % obj.object[:10], 'yellow'),
self.colored('%-10s' % city[:10], 'yellow')
)
result += u' %s \n' % self.colored('%-47s' % obj.fullid[:47], 'blue')
else:
first = True
firstaddress = obj.sensors[0].address
for sensor in obj.sensors:
sensorname = sensor.name
# This is a int value, do not display it as a float
if not empty(sensor.lastvalue.level):
if int(sensor.lastvalue.level) == sensor.lastvalue.level:
lastvalue = "%d " % sensor.lastvalue.level
else:
lastvalue = "%r " % sensor.lastvalue.level
if not empty(sensor.unit):
lastvalue += "%s" % sensor.unit
else:
lastvalue = u"? "
if first:
result = u' %s %s %s ' %\
(self.colored('%-27s' % name[:27], 'red'),
self.colored('%-10s' % obj.object[:10], 'yellow'),
self.colored('%-10s' % city[:10], 'yellow'),
)
if not empty(firstaddress):
result += u'%s' % self.colored('%-33s' % sensor.address[:33], 'yellow')
result += u'\n'
result += u' %s' % self.colored('%-47s' % obj.fullid[:47], 'blue')
result += u' %s %s\n' %\
(self.colored('%-20s' % sensorname[:20], 'magenta'),
self.colored('%-13s' % lastvalue[:13], 'red')
)
first = False
else:
result += u' %s %s\n' %\
(self.colored('%-20s' % sensorname[:20], 'magenta'),
self.colored('%-13s' % lastvalue[:13], 'red')
)
if not empty(sensor.address) and sensor.address != firstaddress:
result += u' %s \n' %\
self.colored('%-33s' % sensor.address[:33], 'yellow')
return result
class Boobsize(ReplApplication):
APPNAME = 'Boobsize'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2013 Florent Fourcot'
DESCRIPTION = "Console application allowing to display various sensors and gauges values."
SHORT_DESCRIPTION = "display sensors and gauges values"
CAPS = (CapGauge)
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {'gauge_list': GaugeFormatter, }
COMMANDS_FORMATTERS = {'search': 'gauge_list',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def bcall_error_handler(self, backend, error, backtrace):
if isinstance(error, SensorNotFound):
msg = unicode(error) or 'Sensor not found (hint: try details command)'
print >>self.stderr, 'Error(%s): %s' % (backend.name, msg)
else:
return ReplApplication.bcall_error_handler(self, backend, error, backtrace)
def do_search(self, pattern):
"""
search [PATTERN]
Display all gauges. If PATTERN is specified, search on a pattern.
"""
self.change_path([u'gauges'])
self.start_format()
for backend, gauge in self.do('iter_gauges', pattern or None, caps=CapGauge):
self.cached_format(gauge)
def complete_search(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_details(self, line):
"""
details GAUGE_ID
Display details of all sensors of the gauge.
"""
gauge, pattern = self.parse_command_args(line, 2, 1)
_id, backend_name = self.parse_id(gauge)
self.start_format()
for backend, sensor in self.do('iter_sensors', _id, pattern=pattern, backends=backend_name, caps=CapGauge):
self.format(sensor)
def do_history(self, line):
"""
history SENSOR_ID
Get history of a specific sensor (use 'search' to find a gauge, and sensors GAUGE_ID to list sensors attached to the gauge).
"""
gauge, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(gauge)
self.start_format()
for backend, measure in self.do('iter_gauge_history', _id, backends=backend_name, caps=CapGauge):
self.format(measure)
def complete_last_sensor_measure(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_last_sensor_measure(self, line):
"""
last_sensor_measure SENSOR_ID
Get last measure of a sensor.
"""
gauge, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(gauge)
self.start_format()
for backend, measure in self.do('get_last_measure', _id, backends=backend_name, caps=CapGauge):
self.format(measure)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobtracker/ 0000775 0000000 0000000 00000000000 12411220504 0026624 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobtracker/__init__.py 0000664 0000000 0000000 00000001433 12411220504 0030736 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .boobtracker import BoobTracker
__all__ = ['BoobTracker']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/boobtracker/boobtracker.py 0000664 0000000 0000000 00000042562 12411220504 0031504 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from datetime import timedelta
from email import message_from_string, message_from_file
from email.Header import decode_header
from email.mime.text import MIMEText
from smtplib import SMTP
import os
import re
import unicodedata
from weboob.capabilities.base import empty, BaseObject
from weboob.capabilities.bugtracker import CapBugTracker, Query, Update, Project, Issue, IssueError
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.tools.html import html2text
from weboob.tools.date import parse_french_date
__all__ = ['BoobTracker']
class IssueFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'project', 'title', 'body', 'author')
def format_attr(self, obj, attr):
if not hasattr(obj, attr) or empty(getattr(obj, attr)):
return u''
value = getattr(obj, attr)
if isinstance(value, BaseObject):
value = value.name
return self.format_key(attr.capitalize(), value)
def format_key(self, key, value):
return '%s %s\n' % (self.colored('%s:' % key, 'green'),
value)
def format_obj(self, obj, alias):
result = u'%s %s %s %s %s\n' % (self.colored(obj.project.name, 'blue', 'bold'),
self.colored(u'—', 'cyan', 'bold'),
self.colored(obj.fullid, 'red', 'bold'),
self.colored(u'—', 'cyan', 'bold'),
self.colored(obj.title, 'yellow', 'bold'))
result += '\n%s\n\n' % obj.body
result += self.format_key('Author', '%s (%s)' % (obj.author.name, obj.creation))
result += self.format_attr(obj, 'status')
result += self.format_attr(obj, 'priority')
result += self.format_attr(obj, 'version')
result += self.format_attr(obj, 'tracker')
result += self.format_attr(obj, 'category')
result += self.format_attr(obj, 'assignee')
if hasattr(obj, 'fields') and not empty(obj.fields):
for key, value in obj.fields.iteritems():
result += self.format_key(key.capitalize(), value)
if hasattr(obj, 'attachments') and obj.attachments:
result += '\n%s\n' % self.colored('Attachments:', 'green')
for a in obj.attachments:
result += '* %s%s%s <%s>\n' % (self.BOLD, a.filename, self.NC, a.url)
if hasattr(obj, 'history') and obj.history:
result += '\n%s\n' % self.colored('History:', 'green')
for u in obj.history:
result += '%s %s %s %s\n' % (self.colored('*', 'red', 'bold'),
self.colored(u.date, 'yellow', 'bold'),
self.colored(u'—', 'cyan', 'bold'),
self.colored(u.author.name, 'blue', 'bold'))
for change in u.changes:
result += ' - %s %s %s %s\n' % (self.colored(change.field, 'green'),
change.last,
self.colored('->', 'magenta'), change.new)
if u.message:
result += ' %s\n' % html2text(u.message).strip().replace('\n', '\n ')
return result
class IssuesListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'project', 'status', 'title', 'category')
def get_title(self, obj):
return '%s - [%s] %s' % (obj.project.name, obj.status.name, obj.title)
def get_description(self, obj):
return obj.category
class BoobTracker(ReplApplication):
APPNAME = 'boobtracker'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2011 Romain Bignon'
DESCRIPTION = "Console application allowing to create, edit, view bug tracking issues."
SHORT_DESCRIPTION = "manage bug tracking issues"
CAPS = CapBugTracker
EXTRA_FORMATTERS = {'issue_info': IssueFormatter,
'issues_list': IssuesListFormatter,
}
COMMANDS_FORMATTERS = {'get': 'issue_info',
'post': 'issue_info',
'edit': 'issue_info',
'search': 'issues_list',
'ls': 'issues_list',
}
COLLECTION_OBJECTS = (Project, Issue, )
def add_application_options(self, group):
group.add_option('--author')
group.add_option('--title')
group.add_option('--assignee')
group.add_option('--target-version', dest='version')
group.add_option('--tracker')
group.add_option('--category')
group.add_option('--status')
group.add_option('--priority')
group.add_option('--start')
group.add_option('--due')
@defaultcount(10)
def do_search(self, line):
"""
search PROJECT
List issues for a project.
You can use these filters from command line:
--author AUTHOR
--title TITLE_PATTERN
--assignee ASSIGNEE
--target-version VERSION
--category CATEGORY
--status STATUS
"""
query = Query()
path = self.working_path.get()
backends = []
if line.strip():
query.project, backends = self.parse_id(line, unique_backend=True)
elif len(path) > 0:
query.project = path[0]
else:
print >>self.stderr, 'Please enter a project name'
return 1
query.author = self.options.author
query.title = self.options.title
query.assignee = self.options.assignee
query.version = self.options.version
query.category = self.options.category
query.status = self.options.status
self.change_path([query.project, u'search'])
for backend, issue in self.do('iter_issues', query, backends=backends):
self.add_object(issue)
self.format(issue)
def complete_get(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_get(self, line):
"""
get ISSUE
Get an issue and display it.
"""
if not line:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('get', short=True)
return 2
issue = self.get_object(line, 'get_issue')
if not issue:
print >>self.stderr, 'Issue not found: %s' % line
return 3
self.format(issue)
def complete_comment(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_comment(self, line):
"""
comment ISSUE [TEXT]
Comment an issue. If no text is given, enter it in standard input.
"""
id, text = self.parse_command_args(line, 2, 1)
if text is None:
text = self.acquire_input()
id, backend_name = self.parse_id(id, unique_backend=True)
update = Update(0)
update.message = text
self.do('update_issue', id, update, backends=backend_name).wait()
def do_logtime(self, line):
"""
logtime ISSUE HOURS [TEXT]
Log spent time on an issue.
"""
id, hours, text = self.parse_command_args(line, 3, 2)
if text is None:
text = self.acquire_input()
try:
hours = float(hours)
except ValueError:
print >>self.stderr, 'Error: HOURS parameter may be a float'
return 1
id, backend_name = self.parse_id(id, unique_backend=True)
update = Update(0)
update.message = text
update.hours = timedelta(hours=hours)
self.do('update_issue', id, update, backends=backend_name).wait()
def complete_remove(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_remove(self, line):
"""
remove ISSUE
Remove an issue.
"""
id, backend_name = self.parse_id(line, unique_backend=True)
self.do('remove_issue', id, backends=backend_name).wait()
ISSUE_FIELDS = (('title', (None, False)),
('assignee', ('members', True)),
('version', ('versions', True)),
('tracker', (None, False)),#XXX
('category', ('categories', False)),
('status', ('statuses', True)),
('priority', (None, False)),#XXX
('start', (None, False)),
('due', (None, False)),
)
def get_list_item(self, objects_list, name):
if name is None:
return None
for obj in objects_list:
if obj.name.lower() == name.lower():
return obj
if not name:
return None
raise ValueError('"%s" is not found' % name)
def sanitize_key(self, key):
if isinstance(key, str):
key = unicode(key, "utf8")
key = unicodedata.normalize('NFKD', key).encode("ascii", "ignore")
return key.replace(' ', '-').capitalize()
def issue2text(self, issue, backend=None):
if backend is not None and 'username' in backend.config:
sender = backend.config['username'].get()
else:
sender = os.environ.get('USERNAME', 'boobtracker')
output = u'From: %s\n' % sender
for key, (list_name, is_list_object) in self.ISSUE_FIELDS:
value = None
if not self.interactive:
value = getattr(self.options, key)
if not value:
value = getattr(issue, key)
if not value:
value = ''
elif hasattr(value, 'name'):
value = value.name
if list_name is not None:
objects_list = getattr(issue.project, list_name)
if len(objects_list) == 0:
continue
output += '%s: %s\n' % (self.sanitize_key(key), value)
if list_name is not None:
availables = ', '.join(['<%s>' % (o if isinstance(o, basestring) else o.name)
for o in objects_list])
output += 'X-Available-%s: %s\n' % (self.sanitize_key(key), availables)
for key, value in issue.fields.iteritems():
output += '%s: %s\n' % (self.sanitize_key(key), value or '')
# TODO: Add X-Available-* for lists
output += '\n%s' % (issue.body or 'Please write your bug report here.')
return output
def text2issue(self, issue, m):
# XXX HACK to support real incoming emails
if 'Subject' in m:
m['Title'] = m['Subject']
for key, (list_name, is_list_object) in self.ISSUE_FIELDS:
value = m.get(key)
if value is None:
continue
new_value = u''
for part in decode_header(value):
if part[1]:
new_value += unicode(part[0], part[1])
else:
new_value += part[0].decode('utf-8')
value = new_value
if is_list_object:
objects_list = getattr(issue.project, list_name)
value = self.get_list_item(objects_list, value)
# FIXME: autodetect
if key in ['start', 'due']:
if len(value) > 0:
#value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
value = parse_french_date(value)
else:
value = None
setattr(issue, key, value)
for key in issue.fields.keys():
value = m.get(self.sanitize_key(key))
if value is not None:
issue.fields[key] = value.decode('utf-8')
content = u''
for part in m.walk():
if part.get_content_type() == 'text/plain':
s = part.get_payload(decode=True)
charsets = part.get_charsets() + m.get_charsets()
for charset in charsets:
try:
if charset is not None:
content += unicode(s, charset)
else:
content += unicode(s, encoding='utf-8')
except UnicodeError as e:
self.logger.warning('Unicode error: %s' % e)
continue
except Exception as e:
self.logger.exception(e)
continue
else:
break
issue.body = content
m = re.search('([^< ]+@[^ >]+)', m['From'] or '')
if m:
return m.group(1)
def edit_issue(self, issue, edit=True):
backend = self.weboob.get_backend(issue.backend)
content = self.issue2text(issue, backend)
while True:
if self.stdin.isatty():
content = self.acquire_input(content, {'vim': "-c 'set ft=mail'"})
m = message_from_string(content.encode('utf-8'))
else:
m = message_from_file(self.stdin)
try:
email_to = self.text2issue(issue, m)
except ValueError as e:
if not self.stdin.isatty():
raise
raw_input("%s -- Press Enter to continue..." % unicode(e).encode("utf-8"))
continue
try:
issue = backend.post_issue(issue)
print 'Issue %s %s' % (self.formatter.colored(issue.fullid, 'red', 'bold'),
'updated' if edit else 'created')
if edit:
self.format(issue)
elif email_to:
self.send_notification(email_to, issue)
return 0
except IssueError as e:
if not self.stdin.isatty():
raise
raw_input("%s -- Press Enter to continue..." % unicode(e).encode("utf-8"))
def send_notification(self, email_to, issue):
text = """Hi,
You have successfuly created this ticket on the Weboob tracker:
%s
You can follow your bug report on this page:
https://symlink.me/issues/%s
Regards,
Weboob Team
""" % (issue.title, issue.id)
msg = MIMEText(text, 'plain', 'utf-8')
msg['Subject'] = 'Issue #%s reported' % issue.id
msg['From'] = 'Weboob '
msg['To'] = email_to
s = SMTP('localhost')
s.sendmail('weboob@weboob.org', [email_to], msg.as_string())
s.quit()
def do_post(self, line):
"""
post PROJECT
Post a new issue.
If you are not in interactive mode, you can use these parameters:
--title TITLE
--assignee ASSIGNEE
--target-version VERSION
--category CATEGORY
--status STATUS
"""
if not line.strip():
print 'Please give the project name'
return 1
project, backend_name = self.parse_id(line, unique_backend=True)
backend = self.weboob.get_backend(backend_name)
issue = backend.create_issue(project)
issue.backend = backend.name
return self.edit_issue(issue, edit=False)
def complete_edit(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
if len(args) == 3:
return dict(self.ISSUE_FIELDS).keys()
def do_edit(self, line):
"""
edit ISSUE [KEY [VALUE]]
Edit an issue.
If you are not in interactive mode, you can use these parameters:
--title TITLE
--assignee ASSIGNEE
--target-version VERSION
--category CATEGORY
--status STATUS
"""
_id, key, value = self.parse_command_args(line, 3, 1)
issue = self.get_object(_id, 'get_issue')
if not issue:
print >>self.stderr, 'Issue not found: %s' % _id
return 3
return self.edit_issue(issue, edit=True)
def complete_attach(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_attach(self, line):
"""
attach ISSUE FILENAME
Attach a file to an issue (Not implemented yet).
"""
print >>self.stderr, 'Not implemented yet.'
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/cineoob/ 0000775 0000000 0000000 00000000000 12411220504 0025745 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/cineoob/__init__.py 0000664 0000000 0000000 00000001421 12411220504 0030054 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .cineoob import Cineoob
__all__ = ['Cineoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/cineoob/cineoob.py 0000664 0000000 0000000 00000061155 12411220504 0027745 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from datetime import datetime
from weboob.applications.weboorrents.weboorrents import TorrentInfoFormatter, TorrentListFormatter
from weboob.applications.suboob.suboob import SubtitleInfoFormatter, SubtitleListFormatter
from weboob.capabilities.torrent import CapTorrent, MagnetOnly
from weboob.capabilities.cinema import CapCinema
from weboob.capabilities.subtitle import CapSubtitle
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.core import CallErrors
__all__ = ['Cineoob']
ROLE_LIST = ['actor', 'director', 'writer', 'composer', 'producer']
COUNTRY_LIST = ['us', 'fr', 'de', 'jp']
class MovieInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'original_title', 'release_date',
'other_titles', 'duration', 'pitch', 'note', 'roles', 'country')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.original_title, self.NC)
result += 'ID: %s\n' % obj.fullid
if not empty(obj.release_date):
result += 'Released: %s\n' % obj.release_date.strftime('%Y-%m-%d')
result += 'Country: %s\n' % obj.country
if not empty(obj.duration):
result += 'Duration: %smin\n' % obj.duration
result += 'Note: %s\n' % obj.note
if not empty(obj.genres):
result += '\n%sGenres%s\n' % (self.BOLD, self.NC)
for g in obj.genres:
result += ' * %s\n' % g
if not empty(obj.roles):
result += '\n%sRelated persons%s\n' % (self.BOLD, self.NC)
for role, lpersons in obj.roles.items():
result += ' -- %s\n' % role
for name in lpersons:
result += ' * %s\n' % name
if not empty(obj.other_titles):
result += '\n%sOther titles%s\n' % (self.BOLD, self.NC)
for t in obj.other_titles:
result += ' * %s\n' % t
if not empty(obj.pitch):
result += '\n%sStory%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.pitch
return result
class MovieListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'original_title', 'short_description')
def get_title(self, obj):
return obj.original_title
def get_description(self, obj):
result = u''
if not empty(obj.short_description):
result = obj.short_description
return result
class MovieReleasesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'original_title', 'all_release_dates')
def get_title(self, obj):
return u'Releases of %s' % obj.original_title
def get_description(self, obj):
return u'\n%s' % obj.all_release_dates
def yearsago(years, from_date=None):
if from_date is None:
from_date = datetime.now()
try:
return from_date.replace(year=from_date.year - years)
except:
# Must be 2/29
assert from_date.month == 2 and from_date.day == 29
return from_date.replace(month=2, day=28,
year=from_date.year-years)
def num_years(begin, end=None):
if end is None:
end = datetime.now()
num_years = int((end - begin).days / 365.25)
if begin > yearsago(num_years, end):
return num_years - 1
else:
return num_years
class PersonInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'name', 'birth_date', 'birth_place', 'short_biography')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.name, self.NC)
result += 'ID: %s\n' % obj.fullid
if not empty(obj.real_name):
result += 'Real name: %s\n' % obj.real_name
if not empty(obj.birth_place):
result += 'Birth place: %s\n' % obj.birth_place
if not empty(obj.birth_date):
result += 'Birth date: %s\n' % obj.birth_date.strftime('%Y-%m-%d')
if not empty(obj.death_date):
age = num_years(obj.birth_date, obj.death_date)
result += 'Death date: %s at %s years old\n' % (obj.death_date.strftime('%Y-%m-%d'), age)
else:
age = num_years(obj.birth_date)
result += 'Age: %s\n' % age
if not empty(obj.gender):
result += 'Gender: %s\n' % obj.gender
if not empty(obj.nationality):
result += 'Nationality: %s\n' % obj.nationality
if not empty(obj.roles):
result += '\n%sRelated movies%s\n' % (self.BOLD, self.NC)
for role, lmovies in obj.roles.items():
result += ' -- %s\n' % role
for movie in lmovies:
result += ' * %s\n' % movie
if not empty(obj.short_biography):
result += '\n%sShort biography%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.short_biography
return result
class PersonListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'short_description')
def get_title(self, obj):
return obj.name
def get_description(self, obj):
result = u''
if not empty(obj.short_description):
result = obj.short_description
return result
class PersonBiographyFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'biography')
def get_title(self, obj):
return u'Biography of %s' % obj.name
def get_description(self, obj):
result = u'\n%s' % obj.biography
return result
class Cineoob(ReplApplication):
APPNAME = 'cineoob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2013 Julien Veyssier'
DESCRIPTION = "Console application allowing to search for movies and persons on various cinema databases " \
", list persons related to a movie, list movies related to a person and list common movies " \
"of two persons."
SHORT_DESCRIPTION = "search movies and persons around cinema"
CAPS = (CapCinema, CapTorrent, CapSubtitle)
EXTRA_FORMATTERS = {'movie_list': MovieListFormatter,
'movie_info': MovieInfoFormatter,
'movie_releases': MovieReleasesFormatter,
'person_list': PersonListFormatter,
'person_info': PersonInfoFormatter,
'person_bio': PersonBiographyFormatter,
'torrent_list': TorrentListFormatter,
'torrent_info': TorrentInfoFormatter,
'subtitle_list': SubtitleListFormatter,
'subtitle_info': SubtitleInfoFormatter
}
COMMANDS_FORMATTERS = {'search_movie': 'movie_list',
'info_movie': 'movie_info',
'search_person': 'person_list',
'info_person': 'person_info',
'casting': 'person_list',
'filmography': 'movie_list',
'biography': 'person_bio',
'releases': 'movie_releases',
'movies_in_common': 'movie_list',
'persons_in_common': 'person_list',
'search_torrent': 'torrent_list',
'search_movie_torrent': 'torrent_list',
'info_torrent': 'torrent_info',
'search_subtitle': 'subtitle_list',
'search_movie_subtitle': 'subtitle_list',
'info_subtitle': 'subtitle_info'
}
def complete_filmography(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 3:
return ROLE_LIST
def complete_casting(self, text, line, *ignored):
return self.complete_filmography(text, line, ignored)
def do_movies_in_common(self, line):
"""
movies_in_common person_ID person_ID
Get the list of common movies between two persons.
"""
id1, id2 = self.parse_command_args(line, 2, 1)
person1 = self.get_object(id1, 'get_person', caps=CapCinema)
if not person1:
print >>self.stderr, 'Person not found: %s' % id1
return 3
person2 = self.get_object(id2, 'get_person', caps=CapCinema)
if not person2:
print >>self.stderr, 'Person not found: %s' % id2
return 3
initial_count = self.options.count
self.options.count = None
lid1 = []
for backend, id in self.do('iter_person_movies_ids', person1.id, caps=CapCinema):
lid1.append(id)
lid2 = []
for backend, id in self.do('iter_person_movies_ids', person2.id, caps=CapCinema):
lid2.append(id)
self.options.count = initial_count
inter = list(set(lid1) & set(lid2))
for common in inter:
movie = self.get_object(common, 'get_movie', caps=CapCinema)
if movie:
self.cached_format(movie)
def do_persons_in_common(self, line):
"""
persons_in_common movie_ID movie_ID
Get the list of common persons between two movies.
"""
id1, id2 = self.parse_command_args(line, 2, 1)
movie1 = self.get_object(id1, 'get_movie', caps=CapCinema)
if not movie1:
print >>self.stderr, 'Movie not found: %s' % id1
return 3
movie2 = self.get_object(id2, 'get_movie', caps=CapCinema)
if not movie2:
print >>self.stderr, 'Movie not found: %s' % id2
return 3
initial_count = self.options.count
self.options.count = None
lid1 = []
for backend, id in self.do('iter_movie_persons_ids', movie1.id, caps=CapCinema):
lid1.append(id)
lid2 = []
for backend, id in self.do('iter_movie_persons_ids', movie2.id, caps=CapCinema):
lid2.append(id)
self.options.count = initial_count
inter = list(set(lid1) & set(lid2))
for common in inter:
person = self.get_object(common, 'get_person', caps=CapCinema)
self.cached_format(person)
def do_info_movie(self, id):
"""
info_movie movie_ID
Get information about a movie.
"""
movie = self.get_object(id, 'get_movie', caps=CapCinema)
if not movie:
print >>self.stderr, 'Movie not found: %s' % id
return 3
self.start_format()
self.format(movie)
def do_info_person(self, id):
"""
info_person person_ID
Get information about a person.
"""
person = self.get_object(id, 'get_person', caps=CapCinema)
if not person:
print >>self.stderr, 'Person not found: %s' % id
return 3
self.start_format()
self.format(person)
@defaultcount(10)
def do_search_movie(self, pattern):
"""
search_movie [PATTERN]
Search movies.
"""
self.change_path([u'search movies'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, movie in self.do('iter_movies', pattern=pattern, caps=CapCinema):
self.cached_format(movie)
@defaultcount(10)
def do_search_person(self, pattern):
"""
search_person [PATTERN]
Search persons.
"""
self.change_path([u'search persons'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, person in self.do('iter_persons', pattern=pattern, caps=CapCinema):
self.cached_format(person)
def do_casting(self, line):
"""
casting movie_ID [ROLE]
List persons related to a movie.
If ROLE is given, filter by ROLE
"""
movie_id, role = self.parse_command_args(line, 2, 1)
movie = self.get_object(movie_id, 'get_movie', caps=CapCinema)
if not movie:
print >>self.stderr, 'Movie not found: %s' % id
return 3
for backend, person in self.do('iter_movie_persons', movie.id, role, backends=movie.backend, caps=CapCinema):
self.cached_format(person)
def do_filmography(self, line):
"""
filmography person_ID [ROLE]
List movies of a person.
If ROLE is given, filter by ROLE
"""
person_id, role = self.parse_command_args(line, 2, 1)
person = self.get_object(person_id, 'get_person', caps=CapCinema)
if not person:
print >>self.stderr, 'Person not found: %s' % id
return 3
for backend, movie in self.do('iter_person_movies', person.id, role, backends=person.backend, caps=CapCinema):
self.cached_format(movie)
def do_biography(self, person_id):
"""
biography person_ID
Show the complete biography of a person.
"""
person = self.get_object(person_id, 'get_person', ('name', 'biography'), caps=CapCinema)
if not person:
print >>self.stderr, 'Person not found: %s' % person_id
return 3
self.start_format()
self.format(person)
def complete_releases(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
if len(args) == 3:
return COUNTRY_LIST
def do_releases(self, line):
"""
releases movie_ID [COUNTRY]
Get releases dates of a movie.
If COUNTRY is given, show release in this country.
"""
id, country = self.parse_command_args(line, 2, 1)
movie = self.get_object(id, 'get_movie', ('original_title'), caps=CapCinema)
if not movie:
print >>self.stderr, 'Movie not found: %s' % id
return 3
# i would like to clarify with fillobj but how could i fill the movie AND choose the country ?
for backend, release in self.do('get_movie_releases', movie.id, country, caps=CapCinema, backends=movie.backend):
if not empty(release):
movie.all_release_dates = u'%s' % (release)
else:
print >>self.stderr, 'Movie releases not found for %s' % movie.original_title
return 3
self.start_format()
self.format(movie)
#================== TORRENT ==================
def complete_info_torrent(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info_torrent(self, id):
"""
info_torrent ID
Get information about a torrent.
"""
torrent = self.get_object(id, 'get_torrent', caps=CapTorrent)
if not torrent:
print >>self.stderr, 'Torrent not found: %s' % id
return 3
self.start_format()
self.format(torrent)
def complete_getfile_torrent(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_getfile_torrent(self, line):
"""
getfile_torrent ID [FILENAME]
Get the .torrent file.
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
_id, backend_name = self.parse_id(id)
if dest is None:
dest = '%s.torrent' % _id
try:
for backend, buf in self.do('get_torrent_file', _id, backends=backend_name, caps=CapTorrent):
if buf:
if dest == '-':
print buf
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print >>self.stderr, 'Unable to write .torrent in "%s": %s' % (dest, e)
return 1
return
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, MagnetOnly):
print >>self.stderr, u'Error(%s): No direct URL available, ' \
u'please provide this magnet URL ' \
u'to your client:\n%s' % (backend, error.magnet)
return 4
else:
self.bcall_error_handler(backend, error, backtrace)
print >>self.stderr, 'Torrent "%s" not found' % id
return 3
@defaultcount(10)
def do_search_torrent(self, pattern):
"""
search_torrent [PATTERN]
Search torrents.
"""
self.change_path([u'search torrent'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, torrent in self.do('iter_torrents', pattern=pattern, caps=CapTorrent):
self.cached_format(torrent)
@defaultcount(10)
def do_search_movie_torrent(self, id):
"""
search_movie_torrent movie_ID
Search torrents of movie_ID.
"""
movie = self.get_object(id, 'get_movie', ('original_title'), caps=CapCinema)
if not movie:
print >>self.stderr, 'Movie not found: %s' % id
return 3
pattern = movie.original_title
self.change_path([u'search torrent'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, torrent in self.do('iter_torrents', pattern=pattern, caps=CapTorrent):
self.cached_format(torrent)
#================== SUBTITLE ==================
def complete_info_subtitle(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info_subtitle(self, id):
"""
info_subtitle subtitle_ID
Get information about a subtitle.
"""
subtitle = self.get_object(id, 'get_subtitle', caps=CapCinema)
if not subtitle:
print >>self.stderr, 'Subtitle not found: %s' % id
return 3
self.start_format()
self.format(subtitle)
def complete_getfile_subtitle(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_getfile_subtitle(self, line):
"""
getfile_subtitle subtitle_ID [FILENAME]
Get the subtitle or archive file.
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
_id, backend_name = self.parse_id(id)
if dest is None:
dest = '%s' % _id
for backend, buf in self.do('get_subtitle_file', _id, backends=backend_name, caps=CapSubtitle):
if buf:
if dest == '-':
print buf
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print >>self.stderr, 'Unable to write file in "%s": %s' % (dest, e)
return 1
return
print >>self.stderr, 'Subtitle "%s" not found' % id
return 3
@defaultcount(10)
def do_search_subtitle(self, line):
"""
search_subtitle language [PATTERN]
Search subtitles.
Language Abbreviation
----------------------
Arabic ar Esperanto eo Irish ga Russian ru
Afrikaans af Estonian et Italian it Serbian sr
Albanian sq Filipino tl Japanese ja Slovak sk
Armenian hy Finnish fi Kannada kn Slovenian sl
Azerbaijani az French fr Korean ko Spanish es
Basque eu Galician gl Latin la Swahili sw
Belarusian be Georgian ka Latvian lv Swedish sv
Bengali bn German de Lithuanian lt Tamil ta
Bulgarian bg Greek gr Macedonian mk Telugu te
Catalan ca Gujarati gu Malay ms Thai th
Chinese zh Haitian ht Maltese mt Turkish tr
Croatian hr Hebrew iw Norwegian no Ukrainian uk
Czech cz Hindi hi Persian fa Urdu ur
Danish da Hungaric hu Polish pl Vietnamese vi
Dutch nl Icelandic is Portuguese pt Welsh cy
English en Indonesian id Romanian ro Yiddish yi
----------------------
"""
language, pattern = self.parse_command_args(line, 2, 1)
self.change_path([u'search subtitle'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, subtitle in self.do('iter_subtitles', language=language, pattern=pattern, caps=CapSubtitle):
self.cached_format(subtitle)
@defaultcount(10)
def do_search_movie_subtitle(self, line):
"""
search_movie_subtitle language movie_ID
Search subtitles of movie_ID.
Language Abbreviation
----------------------
Arabic ar Esperanto eo Irish ga Russian ru
Afrikaans af Estonian et Italian it Serbian sr
Albanian sq Filipino tl Japanese ja Slovak sk
Armenian hy Finnish fi Kannada kn Slovenian sl
Azerbaijani az French fr Korean ko Spanish es
Basque eu Galician gl Latin la Swahili sw
Belarusian be Georgian ka Latvian lv Swedish sv
Bengali bn German de Lithuanian lt Tamil ta
Bulgarian bg Greek gr Macedonian mk Telugu te
Catalan ca Gujarati gu Malay ms Thai th
Chinese zh Haitian ht Maltese mt Turkish tr
Croatian hr Hebrew iw Norwegian no Ukrainian uk
Czech cz Hindi hi Persian fa Urdu ur
Danish da Hungaric hu Polish pl Vietnamese vi
Dutch nl Icelandic is Portuguese pt Welsh cy
English en Indonesian id Romanian ro Yiddish yi
----------------------
"""
language, id = self.parse_command_args(line, 2, 2)
movie = self.get_object(id, 'get_movie', ('original_title'), caps=CapCinema)
if not movie:
print >>self.stderr, 'Movie not found: %s' % id
return 3
pattern = movie.original_title
self.change_path([u'search subtitle'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, subtitle in self.do('iter_subtitles', language=language, pattern=pattern, caps=CapSubtitle):
self.cached_format(subtitle)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/comparoob/ 0000775 0000000 0000000 00000000000 12411220504 0026310 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/comparoob/__init__.py 0000664 0000000 0000000 00000000230 12411220504 0030414 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai
from .comparoob import Comparoob
__all__ = ['Comparoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/comparoob/comparoob.py 0000664 0000000 0000000 00000012276 12411220504 0030653 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.pricecomparison import CapPriceComparison
from weboob.tools.html import html2text
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Comparoob']
class PriceFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'cost', 'currency', 'shop', 'product')
def format_obj(self, obj, alias):
if hasattr(obj, 'message') and obj.message:
message = obj.message
else:
message = u'%s (%s)' % (obj.shop.name, obj.shop.location)
result = u'%s%s%s\n' % (self.BOLD, message, self.NC)
result += u'ID: %s\n' % obj.fullid
result += u'Product: %s\n' % obj.product.name
result += u'Cost: %s%s\n' % (obj.cost, obj.currency)
if hasattr(obj, 'date') and obj.date:
result += u'Date: %s\n' % obj.date.strftime('%Y-%m-%d')
result += u'\n%sShop:%s\n' % (self.BOLD, self.NC)
result += u'\tName: %s\n' % obj.shop.name
if obj.shop.location:
result += u'\tLocation: %s\n' % obj.shop.location
if obj.shop.info:
result += u'\n\t' + html2text(obj.shop.info).replace('\n', '\n\t').strip()
return result
class PricesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'cost', 'currency')
def get_title(self, obj):
if hasattr(obj, 'message') and obj.message:
message = obj.message
elif hasattr(obj, 'shop') and obj.shop:
message = '%s (%s)' % (obj.shop.name, obj.shop.location)
else:
return u'%s%s' % (obj.cost, obj.currency)
return u'%s%s - %s' % (obj.cost, obj.currency, message)
def get_description(self, obj):
if obj.date:
return obj.date.strftime('%Y-%m-%d')
class Comparoob(ReplApplication):
APPNAME = 'comparoob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2012 Romain Bignon'
DESCRIPTION = "Console application to compare products."
SHORT_DESCRIPTION = "compare products"
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {'prices': PricesFormatter,
'price': PriceFormatter,
}
COMMANDS_FORMATTERS = {'prices': 'prices',
'info': 'price',
}
CAPS = CapPriceComparison
def do_prices(self, pattern):
"""
prices [PATTERN]
Display prices for a product. If a pattern is supplied, do not prompt
what product to compare.
"""
products = []
for backend, product in self.do('search_products', pattern):
double = False
for prod in products:
if product.name == prod.name:
double = True
break
if not double:
products.append(product)
product = None
if len(products) == 0:
print >>self.stderr, 'Error: no product found with this pattern'
return 1
elif len(products) == 1:
product = products[0]
else:
print 'What product do you want to compare?'
for i, p in enumerate(products):
print ' %s%2d)%s %s' % (self.BOLD, i+1, self.NC, p.name)
r = int(self.ask(' Select a product', regexp='\d+'))
while product is None:
if r <= 0 or r > len(products):
print 'Error: Please enter a valid ID'
continue
product = products[r-1]
self.change_path([u'prices'])
self.start_format()
products = []
for backend, price in self.do('iter_prices', product):
products.append(price)
for price in sorted(products, key=self._get_price):
self.cached_format(price)
def _get_price(self, price):
return price.cost
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about a product.
"""
if not _id:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True)
return 2
price = self.get_object(_id, 'get_price')
if not price:
print >>self.stderr, 'Price not found: %s' % _id
return 3
self.start_format()
self.format(price)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/cookboob/ 0000775 0000000 0000000 00000000000 12411220504 0026124 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/cookboob/__init__.py 0000664 0000000 0000000 00000001423 12411220504 0030235 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .cookboob import Cookboob
__all__ = ['Cookboob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/cookboob/cookboob.py 0000664 0000000 0000000 00000012414 12411220504 0030275 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import codecs
from weboob.capabilities.recipe import CapRecipe
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Cookboob']
class RecipeInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'title', 'preparation_time', 'ingredients', 'instructions')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.title, self.NC)
result += 'ID: %s\n' % obj.fullid
if not empty(obj.author):
result += 'Author: %s\n' % obj.author
if not empty(obj.preparation_time):
result += 'Preparation time: %smin\n' % obj.preparation_time
if not empty(obj.cooking_time):
result += 'Cooking time: %smin\n' % obj.cooking_time
if not empty(obj.nb_person):
nbstr = '-'.join(str(num) for num in obj.nb_person)
result += 'Amount of people: %s\n' % nbstr
result += '\n%sIngredients%s\n' % (self.BOLD, self.NC)
for i in obj.ingredients:
result += ' * %s\n' % i
result += '\n%sInstructions%s\n' % (self.BOLD, self.NC)
result += '%s\n' % obj.instructions
if not empty(obj.comments):
result += '\n%sComments%s\n' % (self.BOLD, self.NC)
for c in obj.comments:
result += ' * %s\n' % c
return result
class RecipeListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'short_description', 'preparation_time')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
result = u''
if not empty(obj.preparation_time):
result += 'prep time: %smin' % obj.preparation_time
if not empty(obj.short_description):
result += 'description: %s\n' % obj.short_description
return result.strip()
class Cookboob(ReplApplication):
APPNAME = 'cookboob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2013 Julien Veyssier'
DESCRIPTION = "Console application allowing to search for recipes on various websites."
SHORT_DESCRIPTION = "search and consult recipes"
CAPS = CapRecipe
EXTRA_FORMATTERS = {'recipe_list': RecipeListFormatter,
'recipe_info': RecipeInfoFormatter
}
COMMANDS_FORMATTERS = {'search': 'recipe_list',
'info': 'recipe_info'
}
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, id):
"""
info ID
Get information about a recipe.
"""
recipe = self.get_object(id, 'get_recipe')
if not recipe:
print >>self.stderr, 'Recipe not found: %s' % id
return 3
self.start_format()
self.format(recipe)
def complete_export(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_export(self, line):
"""
export ID [FILENAME]
Export the recipe to a KRecipes XML file
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
_id, backend_name = self.parse_id(id)
if dest is None:
dest = '%s.kreml' % _id
recipe = self.get_object(id, 'get_recipe')
if recipe:
xmlstring = recipe.toKrecipesXml(backend_name or None)
if dest == '-':
print xmlstring
else:
if not dest.endswith('.kreml'):
dest += '.kreml'
try:
with codecs.open(dest, 'w', 'utf-8') as f:
f.write(xmlstring)
except IOError as e:
print >>self.stderr, 'Unable to write .kreml in "%s": %s' % (dest, e)
return 1
return
print >>self.stderr, 'Recipe "%s" not found' % id
return 3
@defaultcount(10)
def do_search(self, pattern):
"""
search [PATTERN]
Search recipes.
"""
self.change_path([u'search'])
self.start_format(pattern=pattern)
for backend, recipe in self.do('iter_recipes', pattern=pattern):
self.cached_format(recipe)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/flatboob/ 0000775 0000000 0000000 00000000000 12411220504 0026117 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/flatboob/__init__.py 0000664 0000000 0000000 00000000067 12411220504 0030233 0 ustar 00root root 0000000 0000000 from .flatboob import Flatboob
__all__ = ['Flatboob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/flatboob/flatboob.py 0000664 0000000 0000000 00000016670 12411220504 0030273 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.housing import CapHousing, Query
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Flatboob']
class HousingFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'title', 'cost', 'currency', 'area', 'date', 'text')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.title, self.NC)
result += 'ID: %s\n' % obj.fullid
result += 'Cost: %s%s\n' % (obj.cost, obj.currency)
result += u'Area: %sm²\n' % (obj.area)
if obj.date:
result += 'Date: %s\n' % obj.date.strftime('%Y-%m-%d')
result += 'Phone: %s\n' % obj.phone
if hasattr(obj, 'location') and obj.location:
result += 'Location: %s\n' % obj.location
if hasattr(obj, 'station') and obj.station:
result += 'Station: %s\n' % obj.station
if hasattr(obj, 'photos') and obj.photos:
result += '\n%sPhotos%s\n' % (self.BOLD, self.NC)
for photo in obj.photos:
result += ' * %s\n' % photo.url
result += '\n%sDescription%s\n' % (self.BOLD, self.NC)
result += obj.text
if hasattr(obj, 'details') and obj.details:
result += '\n\n%sDetails%s\n' % (self.BOLD, self.NC)
for key, value in obj.details.iteritems():
result += ' %s: %s\n' % (key, value)
return result
class HousingListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'cost', 'text')
def get_title(self, obj):
return '%s%s - %s' % (obj.cost, obj.currency, obj.title)
def get_description(self, obj):
result = u''
if hasattr(obj, 'date') and obj.date:
result += '%s - ' % obj.date.strftime('%Y-%m-%d')
result += obj.text
return result
class Flatboob(ReplApplication):
APPNAME = 'flatboob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2012 Romain Bignon'
DESCRIPTION = "Console application to search for housing."
SHORT_DESCRIPTION = "search for housing"
CAPS = CapHousing
EXTRA_FORMATTERS = {'housing_list': HousingListFormatter,
'housing': HousingFormatter,
}
COMMANDS_FORMATTERS = {'search': 'housing_list',
'info': 'housing',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
@defaultcount(10)
def do_search(self, line):
"""
search
Search for housing. Parameters are interactively asked.
"""
pattern = 'notempty'
query = Query()
query.cities = []
while pattern:
if len(query.cities) > 0:
print '\n%sSelected cities:%s %s' % (self.BOLD, self.NC, ', '.join([c.name for c in query.cities]))
pattern = self.ask('Enter a city pattern (or empty to stop)', default='')
if not pattern:
break
cities = []
for backend, city in self.weboob.do('search_city', pattern):
cities.append(city)
if len(cities) == 0:
print ' Not found!'
continue
if len(cities) == 1:
if city in query.cities:
query.cities.remove(city)
else:
query.cities.append(city)
continue
r = 'notempty'
while r != '':
for i, city in enumerate(cities):
print ' %s%2d)%s [%s] %s' % (self.BOLD, i+1, self.NC, 'x' if city in query.cities else ' ', city.name)
r = self.ask(' Select cities (or empty to stop)', regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(cities):
continue
city = cities[r-1]
if city in query.cities:
query.cities.remove(city)
else:
query.cities.append(city)
r = 'notempty'
while r != '':
for good in Query.HOUSE_TYPES.values:
print ' %s%2d)%s [%s] %s' % (self.BOLD,
Query.HOUSE_TYPES.index[good] + 1,
self.NC,
'x' if good in query.house_types else ' ', good)
r = self.ask(' Select type of house (or empty to stop)', regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(Query.TYPE_OF_GOOD.values):
continue
value = Query.TYPE_OF_GOOD.values[r - 1]
if value in query.type_of_good:
query.type_of_good.remove(value)
else:
query.type_of_good.append(value)
_type = None
while _type not in [query.TYPE_RENT, query.TYPE_SALE]:
print ' %s%2d)%s %s' % (self.BOLD,
query.TYPE_RENT,
self.NC,
"Rent")
print ' %s%2d)%s %s' % (self.BOLD,
query.TYPE_SALE,
self.NC,
"Sale")
_type = self.ask_int('Type of query')
query.type = _type
query.area_min = self.ask_int('Enter min area')
query.area_max = self.ask_int('Enter max area')
query.cost_min = self.ask_int('Enter min cost')
query.cost_max = self.ask_int('Enter max cost')
query.nb_rooms = self.ask_int('Enter number of rooms')
self.change_path([u'housings'])
self.start_format()
for backend, housing in self.do('search_housings', query):
self.cached_format(housing)
def ask_int(self, txt):
r = self.ask(txt, default='', regexp='(\d+|)')
if r:
return int(r)
return None
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about a housing.
"""
if not _id:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True)
return 2
housing = self.get_object(_id, 'get_housing')
if not housing:
print >>self.stderr, 'Housing not found: %s' % _id
return 3
self.start_format()
self.format(housing)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/galleroob/ 0000775 0000000 0000000 00000000000 12411220504 0026275 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/galleroob/__init__.py 0000664 0000000 0000000 00000001434 12411220504 0030410 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Noé Rubinstein
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .galleroob import Galleroob
__all__ = ['Galleroob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/galleroob/galleroob.py 0000664 0000000 0000000 00000011174 12411220504 0030621 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Noé Rubinstein
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
from re import search, sub
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.capabilities.base import empty
from weboob.capabilities.gallery import CapGallery, BaseGallery, BaseImage
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Galleroob']
class GalleryListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
s = obj.title
if hasattr(obj, 'cardinality') and not empty(obj.cardinality):
s += u' (%d pages)' % obj.cardinality
return s
def get_description(self, obj):
if hasattr(obj, 'description') and obj.description:
return obj.description
class Galleroob(ReplApplication):
APPNAME = 'galleroob'
VERSION = '1.0'
COPYRIGHT = u'Copyright(C) 2011 Noé Rubinstein'
DESCRIPTION = 'galleroob browses and downloads web image galleries'
SHORT_DESCRIPTION = 'browse and download web image galleries'
CAPS = CapGallery
EXTRA_FORMATTERS = {'gallery_list': GalleryListFormatter}
COMMANDS_FORMATTERS = {'search': 'gallery_list', 'ls': 'gallery_list'}
COLLECTION_OBJECTS = (BaseGallery, BaseImage, )
def __init__(self, *args, **kwargs):
ReplApplication.__init__(self, *args, **kwargs)
@defaultcount(10)
def do_search(self, pattern):
"""
search PATTERN
List galleries matching a PATTERN.
"""
if not pattern:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('search', short=True)
return 2
self.start_format(pattern=pattern)
for backend, gallery in self.do('search_gallery', pattern=pattern):
self.cached_format(gallery)
def do_download(self, line):
"""
download ID [FIRST [FOLDER]]
Download a gallery.
Begins at page FIRST (default: 0) and saves to FOLDER (default: title)
"""
_id, first, dest = self.parse_command_args(line, 3, 1)
if first is None:
first = 0
else:
first = int(first)
gallery = None
_id, backend = self.parse_id(_id)
for _backend, result in self.do('get_gallery', _id, backends=backend):
if result:
backend = _backend
gallery = result
if not gallery:
print >>self.stderr, 'Gallery not found: %s' % _id
return 3
backend.fillobj(gallery, ('title',))
if dest is None:
dest = sub('/', ' ', gallery.title)
print "Downloading to %s" % dest
try:
os.mkdir(dest)
except OSError:
pass # ignore error on existing directory
os.chdir(dest) # fail here if dest couldn't be created
i = 0
for img in backend.iter_gallery_images(gallery):
i += 1
if i < first:
continue
backend.fillobj(img, ('url', 'data'))
if img.data is None:
backend.fillobj(img, ('url', 'data'))
if img.data is None:
print >>self.stderr, "Couldn't get page %d, exiting" % i
break
ext = search(r"\.([^\.]{1,5})$", img.url)
if ext:
ext = ext.group(1)
else:
ext = "jpg"
name = '%03d.%s' % (i, ext)
print 'Writing file %s' % name
with open(name, 'w') as f:
f.write(img.data)
os.chdir(os.path.pardir)
def do_info(self, line):
"""
info ID
Get information about a gallery.
"""
_id, = self.parse_command_args(line, 1, 1)
gallery = self.get_object(_id, 'get_gallery')
if not gallery:
print >>self.stderr, 'Gallery not found: %s' % _id
return 3
self.start_format()
self.format(gallery)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/geolooc/ 0000775 0000000 0000000 00000000000 12411220504 0025756 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/geolooc/__init__.py 0000664 0000000 0000000 00000001530 12411220504 0030066 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .geolooc import Geolooc
__all__ = ['Geolooc']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/geolooc/geolooc.py 0000664 0000000 0000000 00000002564 12411220504 0027766 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.geolocip import CapGeolocIp
from weboob.tools.application.repl import ReplApplication
__all__ = ['Geolooc']
class Geolooc(ReplApplication):
APPNAME = 'geolooc'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Romain Bignon'
DESCRIPTION = "Console application allowing to geolocalize IP addresses."
SHORT_DESCRIPTION = "geolocalize IP addresses"
CAPS = CapGeolocIp
def main(self, argv):
if len(argv) < 2:
print >>self.stderr, 'Syntax: %s ipaddr' % argv[0]
return 2
for backend, location in self.do('get_location', argv[1]):
self.format(location)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/handjoob/ 0000775 0000000 0000000 00000000000 12411220504 0026113 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/handjoob/__init__.py 0000664 0000000 0000000 00000001415 12411220504 0030225 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .handjoob import Handjoob
__all__ = ['Handjoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/handjoob/handjoob.py 0000664 0000000 0000000 00000011505 12411220504 0030253 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.job import CapJob
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Handjoob']
class JobAdvertFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'url', 'publication_date', 'title')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.title, self.NC)
result += 'url: %s\n' % obj.url
if hasattr(obj, 'publication_date') and obj.publication_date:
result += 'Publication date : %s\n' % obj.publication_date.strftime('%Y-%m-%d')
if hasattr(obj, 'place') and obj.place:
result += 'Location: %s\n' % obj.place
if hasattr(obj, 'society_name') and obj.society_name:
result += 'Society : %s\n' % obj.society_name
if hasattr(obj, 'job_name') and obj.job_name:
result += 'Job name : %s\n' % obj.job_name
if hasattr(obj, 'contract_type') and obj.contract_type:
result += 'Contract : %s\n' % obj.contract_type
if hasattr(obj, 'pay') and obj.pay:
result += 'Pay : %s\n' % obj.pay
if hasattr(obj, 'formation') and obj.formation:
result += 'Formation : %s\n' % obj.formation
if hasattr(obj, 'experience') and obj.experience:
result += 'Experience : %s\n' % obj.experience
if hasattr(obj, 'description') and obj.description:
result += 'Description : %s\n' % obj.description
return result
class JobAdvertListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
return '%s' % (obj.title)
def get_description(self, obj):
result = u''
if hasattr(obj, 'publication_date') and obj.publication_date:
result += '\tPublication date : %s\n' % obj.publication_date.strftime('%Y-%m-%d')
if hasattr(obj, 'place') and obj.place:
result += '\tLocation: %s\n' % obj.place
if hasattr(obj, 'society_name') and obj.society_name:
result += '\tSociety : %s\n' % obj.society_name
if hasattr(obj, 'contract_type') and obj.contract_type:
result += '\tContract : %s\n' % obj.contract_type
return result.strip('\n\t')
class Handjoob(ReplApplication):
APPNAME = 'handjoob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2012 Bezleputh'
DESCRIPTION = "Console application to search for a job."
SHORT_DESCRIPTION = "search for a job"
CAPS = CapJob
EXTRA_FORMATTERS = {'job_advert_list': JobAdvertListFormatter,
'job_advert': JobAdvertFormatter,
}
COMMANDS_FORMATTERS = {'search': 'job_advert_list',
'ls': 'job_advert_list',
'info': 'job_advert',
}
@defaultcount(10)
def do_search(self, pattern):
"""
search PATTERN
Search for an advert matching a PATTERN.
"""
self.change_path([u'search'])
self.start_format(pattern=pattern)
for backend, job_advert in self.do('search_job', pattern):
self.cached_format(job_advert)
@defaultcount(10)
def do_ls(self, line):
"""
advanced search
Search for an advert matching to advanced filters.
"""
self.change_path([u'advanced'])
for backend, job_advert in self.do('advanced_search_job'):
self.cached_format(job_advert)
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about an advert.
"""
if not _id:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True)
return 2
job_advert = self.get_object(_id, 'get_job_advert')
if not job_advert:
print >>self.stderr, 'Job advert not found: %s' % _id
return 3
self.start_format()
self.format(job_advert)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/havedate/ 0000775 0000000 0000000 00000000000 12411220504 0026110 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/havedate/__init__.py 0000664 0000000 0000000 00000001427 12411220504 0030225 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .havedate import HaveDate
__all__ = ['HaveDate']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/havedate/havedate.py 0000664 0000000 0000000 00000024160 12411220504 0030246 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from copy import copy
from weboob.core import CallErrors
from weboob.tools.application.repl import ReplApplication
from weboob.applications.boobmsg import Boobmsg
from weboob.capabilities.dating import CapDating, OptimizationNotFound
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['HaveDate']
class EventListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('date', 'type')
def get_title(self, event):
s = u'(%s) %s' % (event.date, event.type)
if hasattr(event, 'contact') and event.contact:
s += u' — %s (%s)' % (event.contact.name, event.contact.id)
return s
def get_description(self, event):
if hasattr(event, 'message'):
return event.message
class HaveDate(Boobmsg):
APPNAME = 'havedate'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2012 Romain Bignon'
DESCRIPTION = "Console application allowing to interact with various dating websites " \
"and to optimize seduction algorithmically."
SHORT_DESCRIPTION = "interact with dating websites"
STORAGE_FILENAME = 'dating.storage'
STORAGE = {'optims': {}}
CAPS = CapDating
EXTRA_FORMATTERS = copy(Boobmsg.EXTRA_FORMATTERS)
EXTRA_FORMATTERS['events'] = EventListFormatter
COMMANDS_FORMATTERS = copy(Boobmsg.COMMANDS_FORMATTERS)
COMMANDS_FORMATTERS['optim'] = 'table'
COMMANDS_FORMATTERS['events'] = 'events'
def load_default_backends(self):
self.load_backends(CapDating, storage=self.create_storage(self.STORAGE_FILENAME))
def main(self, argv):
self.load_config()
try:
self.do('init_optimizations').wait()
except CallErrors as e:
self.bcall_errors_handler(e)
optimizations = self.storage.get('optims')
for optim, backends in optimizations.iteritems():
self.optims('start', backends, optim, store=False)
return ReplApplication.main(self, argv)
def do_query(self, id):
"""
query ID
Send a query to someone.
"""
_id, backend_name = self.parse_id(id, unique_backend=True)
for backend, query in self.do('send_query', _id, backends=backend_name):
print '%s' % query.message
def edit_optims(self, backend_names, optims_names, stop=False):
if optims_names is None:
print >>self.stderr, 'Error: missing parameters.'
return 2
for optim_name in optims_names.split():
backends_optims = {}
for backend, optim in self.do('get_optimization', optim_name, backends=backend_names):
if optim:
backends_optims[backend.name] = optim
for backend_name, optim in backends_optims.iteritems():
if len(optim.CONFIG) == 0:
print '%s.%s does not require configuration.' % (backend_name, optim_name)
continue
was_running = optim.is_running()
if stop and was_running:
print 'Stopping %s: %s' % (optim_name, backend_name)
optim.stop()
params = optim.get_config()
if params is None:
params = {}
print 'Configuration of %s.%s' % (backend_name, optim_name)
print '-----------------%s-%s' % ('-' * len(backend_name), '-' * len(optim_name))
for key, value in optim.CONFIG.iteritems():
params[key] = self.ask(value, default=params[key] if (key in params) else value.default)
optim.set_config(params)
if stop and was_running:
print 'Starting %s: %s' % (optim_name, backend_name)
optim.start()
def optims(self, function, backend_names, optims, store=True):
if optims is None:
print >>self.stderr, 'Error: missing parameters.'
return 2
for optim_name in optims.split():
try:
if store:
storage_optim = set(self.storage.get('optims', optim_name, default=[]))
self.stdout.write('%sing %s:' % (function.capitalize(), optim_name))
for backend, optim in self.do('get_optimization', optim_name, backends=backend_names):
if optim:
# It's useless to start a started optim, or to stop a stopped one.
if (function == 'start' and optim.is_running()) or \
(function == 'stop' and not optim.is_running()):
continue
# Optim is not configured and would be, ask user to do it.
if function == 'start' and len(optim.CONFIG) > 0 and optim.get_config() is None:
self.edit_optims(backend.name, optim_name)
ret = getattr(optim, function)()
self.stdout.write(' ' + backend.name)
if not ret:
self.stdout.write('(failed)')
self.stdout.flush()
if store:
if function == 'start' and ret:
storage_optim.add(backend.name)
elif function == 'stop':
try:
storage_optim.remove(backend.name)
except KeyError:
pass
self.stdout.write('.\n')
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, OptimizationNotFound):
self.logger.error(u'Error(%s): Optimization "%s" not found' % (backend.name, optim_name))
else:
self.bcall_error_handler(backend, error, backtrace)
if store:
if len(storage_optim) > 0:
self.storage.set('optims', optim_name, list(storage_optim))
else:
self.storage.delete('optims', optim_name)
if store:
self.storage.save()
def complete_optim(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return ['list', 'start', 'stop', 'edit']
elif len(args) == 3:
return [backend.name for backend in self.enabled_backends]
elif len(args) >= 4:
if args[2] == '*':
backend = None
else:
backend = args[2]
optims = set()
for backend, (name, optim) in self.do('iter_optimizations', backends=backend):
optims.add(name)
return sorted(optims - set(args[3:]))
def do_optim(self, line):
"""
optim [list | start | edit | stop] BACKEND [OPTIM1 [OPTIM2 ...]]
All dating backends offer optimization services. This command can be
manage them.
Use * us BACKEND value to apply command to all backends.
Commands:
* list list all available optimizations of a backend
* start start optimization services on a backend
* edit configure an optimization service for a backend
* stop stop optimization services on a backend
"""
cmd, backend_name, optims_names = self.parse_command_args(line, 3)
if backend_name == '*':
backend_name = None
elif backend_name is not None and not backend_name in [b.name for b in self.enabled_backends]:
print >>self.stderr, 'Error: No such backend "%s"' % backend_name
return 1
if cmd == 'start':
return self.optims('start', backend_name, optims_names)
if cmd == 'stop':
return self.optims('stop', backend_name, optims_names)
if cmd == 'edit':
self.edit_optims(backend_name, optims_names, stop=True)
return
if cmd == 'list' or cmd is None:
if optims_names is not None:
optims_names = optims_names.split()
optims = {}
backends = set()
for backend, (name, optim) in self.do('iter_optimizations', backends=backend_name):
if optims_names is not None and not name in optims_names:
continue
if optim.is_running():
status = 'RUNNING'
else:
status = '-------'
if not name in optims:
optims[name] = {backend.name: status}
else:
optims[name][backend.name] = status
backends.add(backend.name)
backends = sorted(backends)
for name, backends_status in optims.iteritems():
line = [('name', name)]
for b in backends:
try:
status = backends_status[b]
except KeyError:
status = ''
line.append((b, status))
self.format(tuple(line))
return
print >>self.stderr, "No such command '%s'" % cmd
return 1
def do_events(self, line):
"""
events
Display dating events.
"""
self.change_path([u'events'])
self.start_format()
for backend, event in self.do('iter_events'):
self.cached_format(event)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/masstransit/ 0000775 0000000 0000000 00000000000 12411220504 0026677 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/masstransit/__init__.py 0000664 0000000 0000000 00000001544 12411220504 0031014 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .masstransit import Masstransit
__all__ = ['Masstransit']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/masstransit/masstransit.py 0000664 0000000 0000000 00000024344 12411220504 0031630 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Julien Hébert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.travel import CapTravel
from weboob.tools.application.base import Application
from logging import warning
import gtk
class FakeConic(object):
STATUS_CONNECTED = None
STATUS_DISCONNECTED = None
CONNECT_FLAG_NONE = None
def Connection(self):
raise NotImplementedError()
try:
import hildon
except ImportError:
toolkit = gtk
else:
toolkit = hildon
try:
import conic
except ImportError:
warning("conic is not found")
conic = FakeConic()
from logging import debug
__all__ = ['Masstransit']
class MasstransitHildon():
"hildon interface"
def connect_event(self, connection, event=None, c=None, d=None):
debug("DBUS-DEBUG a: %s, b:%s, c:%s,d: %s" % (connection, event, c, d))
status = event.get_status()
if status == conic.STATUS_CONNECTED:
self.connected = True
if not self.touch_selector_entry_filled:
debug("connected, now fill")
self.fill_touch_selector_entry()
if self.refresh_in_progress:
self.refresh()
elif status == conic.STATUS_DISCONNECTED:
self.connected = False
def __init__(self, weboob):
self.touch_selector_entry_filled = False
self.refresh_in_progress = False
self.connected = False
self.weboob = weboob
try:
self.connection = conic.Connection()
self.connection.connect("connection-event", self.connect_event)
self.connection.set_property("automatic-connection-events", True)
self.connection.request_connection(conic.CONNECT_FLAG_NONE)
except NotImplementedError:
pass
horizontal_box = gtk.HBox()
self.main_window = toolkit.Window()
try:
self.refresh_button = toolkit.Button(
gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
hildon.BUTTON_ARRANGEMENT_HORIZONTAL,
"Actualiser"
)
self.retour_button = hildon.Button(
gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
hildon.BUTTON_ARRANGEMENT_HORIZONTAL,
"Retour"
)
self.combo_source = hildon.TouchSelectorEntry(text=True)
self.combo_dest = hildon.TouchSelectorEntry(text=True)
self.picker_button_source = hildon.PickerButton(
gtk.HILDON_SIZE_AUTO,
hildon.BUTTON_ARRANGEMENT_VERTICAL)
self.picker_button_dest = hildon.PickerButton(
gtk.HILDON_SIZE_AUTO,
hildon.BUTTON_ARRANGEMENT_VERTICAL
)
self.picker_button_source.set_sensitive(False)
self.picker_button_dest.set_sensitive(False)
self.picker_button_source.set_title("Gare de Depart")
self.picker_button_dest.set_title("Gare d'arrivee")
self.picker_button_source.set_selector(self.combo_source)
self.picker_button_dest.set_selector(self.combo_dest)
horizontal_box.pack_start(self.picker_button_source)
horizontal_box.pack_start(self.picker_button_dest)
except AttributeError:
self.refresh_button = gtk.Button("Actualiser")
self.retour_button = gtk.Button("Retour")
self.combo_source = gtk.combo_box_entry_new_text()
self.combo_dest = gtk.combo_box_entry_new_text()
horizontal_box.pack_start(self.combo_source)
horizontal_box.pack_start(self.combo_dest)
self.main_window.set_title("Horaires des Prochains Trains")
self.main_window.connect("destroy", self.on_main_window_destroy)
self.refresh_button.connect("clicked", self.on_refresh_button_clicked)
self.retour_button.set_sensitive(False)
self.retour_button.connect("clicked", self.on_retour_button_clicked)
self.treestore = gtk.TreeStore(str, str, str, str, str)
treeview = gtk.TreeView(self.treestore)
treeview.append_column(
gtk.TreeViewColumn(
'Train',
gtk.CellRendererText(),
text=0
))
treeview.append_column(
gtk.TreeViewColumn(
'Horaire',
gtk.CellRendererText(),
text=1
))
treeview.append_column(
gtk.TreeViewColumn(
'Destination',
gtk.CellRendererText(),
text=2
))
treeview.append_column(
gtk.TreeViewColumn(
'Voie',
gtk.CellRendererText(),
text=3
))
treeview.append_column(
gtk.TreeViewColumn(
'Information',
gtk.CellRendererText(),
text=4
))
vertical_box = gtk.VBox()
vertical_box.pack_start(horizontal_box)
horizontal_box.pack_start(self.retour_button)
vertical_box.pack_start(treeview)
vertical_box.pack_start(self.refresh_button)
self.main_window.add(vertical_box)
self.main_window.show_all()
self.fill_touch_selector_entry()
if toolkit != gtk:
self.picker_button_source.connect("value-changed",
self.check_station_input,
self.picker_button_source)
self.picker_button_dest.connect("value-changed",
self.check_station_input,
self.picker_button_dest)
def fill_touch_selector_entry(self):
liste = []
for backend in self.weboob.iter_backends():
for station in backend.iter_station_search(""):
liste.append(station.name.capitalize())
liste.sort()
for station in liste:
self.combo_source.append_text(station)
self.combo_dest.append_text(station)
self.touch_selector_entry_filled = True
if toolkit != gtk:
self.picker_button_source.set_sensitive(True)
def on_main_window_destroy(self, widget):
"exit application at the window close"
gtk.main_quit()
def on_main_window_show(self, param):
self.fill_touch_selector_entry()
def on_retour_button_clicked(self, widget):
"the button is clicked"
debug("on_retour_button_clicked")
self.refresh_in_progress = True
col_source = self.combo_source.get_active(0)
col_dest = self.combo_dest.get_active(0)
self.combo_source.set_active(0, col_dest)
self.combo_dest.set_active(0, col_source)
self.refresh()
def on_refresh_button_clicked(self, widget):
"the refresh button is clicked"
debug("on_refresh_button_clicked")
self.refresh_in_progress = True
try:
self.connection.request_connection(conic.CONNECT_FLAG_NONE)
except AttributeError:
if isinstance(conic, FakeConic):
self.refresh()
else:
raise
def check_station_input(self, widget, user_data):
if self.combo_source.get_current_text() is None:
self.picker_button_dest.set_sensitive(False)
self.refresh_button.set_sensitive(False)
self.retour_button.set_sensitive(False)
else:
self.picker_button_dest.set_sensitive(True)
if self.combo_dest.get_current_text() is None:
self.refresh_button.set_sensitive(False)
self.retour_button.set_sensitive(False)
else:
self.refresh_button.set_sensitive(True)
self.retour_button.set_sensitive(True)
def refresh(self):
"update departures"
banner = hildon.hildon_banner_show_information(self.main_window, "", "Chargement en cours")
banner.set_timeout(10000)
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, 1)
self.treestore.clear()
try:
source_text = self.combo_source.get_current_text()
dest_text = self.combo_dest.get_current_text()
except AttributeError:
source_text = self.combo_source.child.get_text()
dest_text = self.combo_dest.child.get_text()
for backend in self.weboob.iter_backends():
for station in backend.iter_station_search(source_text):
for arrival in \
backend.iter_station_search(dest_text):
for departure in \
backend.iter_station_departures(station.id, arrival.id):
self.treestore.append(None,
[departure.type,
departure.time,
departure.arrival_station,
departure.plateform,
departure.information])
self.refresh_in_progress = False
banner.set_timeout(1)
hildon.hildon_gtk_window_set_progress_indicator(self.main_window, 0)
class Masstransit(Application):
"Application Class"
APPNAME = 'masstransit'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Julien Hébert'
DESCRIPTION = "Maemo application allowing to search for train stations and get departure times."
SHORT_DESCRIPTION = "search for train stations and departures"
def main(self, argv):
self.load_backends(CapTravel)
MasstransitHildon(self.weboob)
gtk.main()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/monboob/ 0000775 0000000 0000000 00000000000 12411220504 0025762 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/monboob/__init__.py 0000664 0000000 0000000 00000001424 12411220504 0030074 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .monboob import Monboob
__all__ = ['Monboob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/monboob/monboob.py 0000664 0000000 0000000 00000032032 12411220504 0027767 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2009-2011 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from email.mime.text import MIMEText
from smtplib import SMTP
from email.Header import Header, decode_header
from email.Utils import parseaddr, formataddr, formatdate
from email import message_from_file, message_from_string
from smtpd import SMTPServer
import time
import re
import logging
import asyncore
import subprocess
import socket
from weboob.core import Weboob, CallErrors
from weboob.core.scheduler import Scheduler
from weboob.capabilities.messages import CapMessages, CapMessagesPost, Thread, Message
from weboob.tools.application.repl import ReplApplication
from weboob.tools.date import utc2local
from weboob.tools.html import html2text
from weboob.tools.misc import get_backtrace, to_unicode
__all__ = ['Monboob']
class FakeSMTPD(SMTPServer):
def __init__(self, app, bindaddr, port):
SMTPServer.__init__(self, (bindaddr, port), None)
self.app = app
def process_message(self, peer, mailfrom, rcpttos, data):
msg = message_from_string(data)
self.app.process_incoming_mail(msg)
class MonboobScheduler(Scheduler):
def __init__(self, app):
Scheduler.__init__(self)
self.app = app
def run(self):
if self.app.options.smtpd:
if ':' in self.app.options.smtpd:
host, port = self.app.options.smtpd.split(':', 1)
else:
host = '127.0.0.1'
port = self.app.options.smtpd
try:
FakeSMTPD(self.app, host, int(port))
except socket.error as e:
self.logger.error('Unable to start the SMTP daemon: %s' % e)
return False
# XXX Fuck, we shouldn't copy this piece of code from
# weboob.scheduler.Scheduler.run().
try:
while True:
self.stop_event.wait(0.1)
if self.app.options.smtpd:
asyncore.loop(timeout=0.1, count=1)
except KeyboardInterrupt:
self._wait_to_stop()
raise
else:
self._wait_to_stop()
return True
class Monboob(ReplApplication):
APPNAME = 'monboob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Romain Bignon'
DESCRIPTION = 'Daemon allowing to regularly check for new messages on various websites, ' \
'and send an email for each message, and post a reply to a message on a website.'
SHORT_DESCRIPTION = "daemon to send and check messages"
CONFIG = {'interval': 300,
'domain': 'weboob.example.org',
'recipient': 'weboob@example.org',
'smtp': 'localhost',
'pipe': '',
'html': 0}
CAPS = CapMessages
DISABLE_REPL = True
def add_application_options(self, group):
group.add_option('-S', '--smtpd', help='run a fake smtpd server and set the port')
def create_weboob(self):
return Weboob(scheduler=MonboobScheduler(self))
def load_default_backends(self):
self.load_backends(CapMessages, storage=self.create_storage())
def main(self, argv):
self.load_config()
try:
self.config.set('interval', int(self.config.get('interval')))
if self.config.get('interval') < 1:
raise ValueError()
except ValueError:
print >>self.stderr, 'Configuration error: interval must be an integer >0.'
return 1
try:
self.config.set('html', int(self.config.get('html')))
if self.config.get('html') not in (0, 1):
raise ValueError()
except ValueError:
print >>self.stderr, 'Configuration error: html must be 0 or 1.'
return 2
return ReplApplication.main(self, argv)
def get_email_address_ident(self, msg, header):
s = msg.get(header)
if not s:
return None
m = re.match('.*<([^@]*)@(.*)>', s)
if m:
return m.group(1)
else:
try:
return s.split('@')[0]
except IndexError:
return s
def do_post(self, line):
"""
post
Pipe with a mail to post message.
"""
msg = message_from_file(self.stdin)
return self.process_incoming_mail(msg)
def process_incoming_mail(self, msg):
to = self.get_email_address_ident(msg, 'To')
sender = msg.get('From')
reply_to = self.get_email_address_ident(msg, 'In-Reply-To')
title = msg.get('Subject')
if title:
new_title = u''
for part in decode_header(title):
if part[1]:
new_title += unicode(part[0], part[1])
else:
new_title += unicode(part[0])
title = new_title
content = u''
for part in msg.walk():
if part.get_content_type() == 'text/plain':
s = part.get_payload(decode=True)
charsets = part.get_charsets() + msg.get_charsets()
for charset in charsets:
try:
if charset is not None:
content += unicode(s, charset)
else:
content += unicode(s)
except UnicodeError as e:
self.logger.warning('Unicode error: %s' % e)
continue
except Exception as e:
self.logger.exception(e)
continue
else:
break
if len(content) == 0:
print >>self.stderr, 'Unable to send an empty message'
return 1
# remove signature
content = content.split(u'\n-- \n')[0]
parent_id = None
if reply_to is None:
# This is a new message
if '.' in to:
bname, thread_id = to.split('.', 1)
else:
bname = to
thread_id = None
else:
# This is a reply
try:
bname, id = reply_to.split('.', 1)
thread_id, parent_id = id.rsplit('.', 1)
except ValueError:
print >>self.stderr, 'In-Reply-To header might be in form '
return 1
# Default use the To header field to know the backend to use.
if to and bname != to:
bname = to
try:
backend = self.weboob.backend_instances[bname]
except KeyError:
print >>self.stderr, 'Backend %s not found' % bname
return 1
if not backend.has_caps(CapMessagesPost):
print >>self.stderr, 'The backend %s does not implement CapMessagesPost' % bname
return 1
thread = Thread(thread_id)
message = Message(thread,
0,
title=title,
sender=sender,
receivers=[to],
parent=Message(thread, parent_id) if parent_id else None,
content=content)
try:
backend.post_message(message)
except Exception as e:
content = u'Unable to send message to %s:\n' % thread_id
content += u'\n\t%s\n' % to_unicode(e)
if logging.root.level == logging.DEBUG:
content += u'\n%s\n' % to_unicode(get_backtrace(e))
self.send_email(backend, Message(thread,
0,
title='Unable to send message',
sender='Monboob',
parent=Message(thread, parent_id) if parent_id else None,
content=content))
def do_run(self, line):
"""
run
Run the fetching daemon.
"""
self.weboob.repeat(self.config.get('interval'), self.process)
self.weboob.loop()
def do_once(self, line):
"""
once
Send mails only once, then exit.
"""
return self.process()
def process(self):
try:
for backend, message in self.weboob.do('iter_unread_messages'):
if self.send_email(backend, message):
backend.set_message_read(message)
except CallErrors as e:
self.bcall_errors_handler(e)
def send_email(self, backend, mail):
domain = self.config.get('domain')
recipient = self.config.get('recipient')
reply_id = ''
if mail.parent:
reply_id = u'<%s.%s@%s>' % (backend.name, mail.parent.full_id, domain)
subject = mail.title
sender = u'"%s" <%s@%s>' % (mail.sender.replace('"', '""') if mail.sender else '',
backend.name, domain)
# assume that .date is an UTC datetime
date = formatdate(time.mktime(utc2local(mail.date).timetuple()), localtime=True)
msg_id = u'<%s.%s@%s>' % (backend.name, mail.full_id, domain)
if self.config.get('html') and mail.flags & mail.IS_HTML:
body = mail.content
content_type = 'html'
else:
if mail.flags & mail.IS_HTML:
body = html2text(mail.content)
else:
body = mail.content
content_type = 'plain'
if body is None:
body = ''
if mail.signature:
if self.config.get('html') and mail.flags & mail.IS_HTML:
body += u'
-- %s
' % mail.signature
else:
body += u'\n\n-- \n'
if mail.flags & mail.IS_HTML:
body += html2text(mail.signature)
else:
body += mail.signature
# Header class is smart enough to try US-ASCII, then the charset we
# provide, then fall back to UTF-8.
header_charset = 'ISO-8859-1'
# We must choose the body charset manually
for body_charset in 'US-ASCII', 'ISO-8859-1', 'UTF-8':
try:
body.encode(body_charset)
except UnicodeError:
pass
else:
break
# Split real name (which is optional) and email address parts
sender_name, sender_addr = parseaddr(sender)
recipient_name, recipient_addr = parseaddr(recipient)
# We must always pass Unicode strings to Header, otherwise it will
# use RFC 2047 encoding even on plain ASCII strings.
sender_name = str(Header(unicode(sender_name), header_charset))
recipient_name = str(Header(unicode(recipient_name), header_charset))
# Make sure email addresses do not contain non-ASCII characters
sender_addr = sender_addr.encode('ascii')
recipient_addr = recipient_addr.encode('ascii')
# Create the message ('plain' stands for Content-Type: text/plain)
msg = MIMEText(body.encode(body_charset), content_type, body_charset)
msg['From'] = formataddr((sender_name, sender_addr))
msg['To'] = formataddr((recipient_name, recipient_addr))
msg['Subject'] = Header(unicode(subject), header_charset)
msg['Message-Id'] = msg_id
msg['Date'] = date
if reply_id:
msg['In-Reply-To'] = reply_id
self.logger.info('Send mail from <%s> to <%s>' % (sender, recipient))
if len(self.config.get('pipe')) > 0:
p = subprocess.Popen(self.config.get('pipe'),
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
p.stdin.write(msg.as_string())
p.stdin.close()
if p.wait() != 0:
self.logger.error('Unable to deliver mail: %s' % p.stdout.read().strip())
return False
else:
# Send the message via SMTP to localhost:25
try:
smtp = SMTP(self.config.get('smtp'))
smtp.sendmail(sender, recipient, msg.as_string())
except Exception as e:
self.logger.error('Unable to deliver mail: %s' % e)
return False
else:
smtp.quit()
return True
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/parceloob/ 0000775 0000000 0000000 00000000000 12411220504 0026275 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/parceloob/__init__.py 0000664 0000000 0000000 00000001425 12411220504 0030410 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .parceloob import Parceloob
__all__ = ['Parceloob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/parceloob/parceloob.py 0000664 0000000 0000000 00000015421 12411220504 0030620 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.base import empty
from weboob.capabilities.parcel import CapParcel, Parcel, ParcelNotFound
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['Parceloob']
STATUS = {Parcel.STATUS_PLANNED: ('PLANNED', 'red'),
Parcel.STATUS_IN_TRANSIT: ('IN TRANSIT', 'yellow'),
Parcel.STATUS_ARRIVED: ('ARRIVED', 'green'),
Parcel.STATUS_UNKNOWN: ('', 'white'),
}
def get_backend_name(backend):
return backend.name
class HistoryFormatter(IFormatter):
MANDATORY_FIELDS = ()
def format_obj(self, obj, alias):
if isinstance(obj, Parcel):
result = u'Parcel %s (%s)\n' % (self.colored(obj.id, 'red', 'bold'),
self.colored(obj.backend, 'blue', 'bold'))
result += u'%sArrival:%s %s\n' % (self.BOLD, self.NC, obj.arrival)
status, status_color = STATUS[obj.status]
result += u'%sStatus:%s %s\n' % (self.BOLD, self.NC, self.colored(status, status_color))
result += u'%sInfo:%s %s\n\n' % (self.BOLD, self.NC, obj.info)
result += u' Date Location Activity \n'
result += u'---------------------+-----------------+---------------------------------------------------'
return result
return ' %s %s %s' % (self.colored('%-19s' % obj.date, 'blue'),
self.colored('%-17s' % (obj.location or ''), 'magenta'),
self.colored(obj.activity or '', 'yellow'))
class StatusFormatter(IFormatter):
MANDATORY_FIELDS = ('id',)
def format_obj(self, obj, alias):
if alias is not None:
id = '%s (%s)' % (self.colored('%3s' % ('#' + alias), 'red', 'bold'),
self.colored(obj.backend, 'blue', 'bold'))
clean = '#%s (%s)' % (alias, obj.backend)
if len(clean) < 15:
id += (' ' * (15 - len(clean)))
else:
id = self.colored('%30s' % obj.fullid, 'red', 'bold')
status, status_color = STATUS[obj.status]
arrival = obj.arrival.strftime('%Y-%m-%d') if not empty(obj.arrival) else ''
result = u'%s %s %s %s %s' % (id, self.colored(u'—', 'cyan'),
self.colored('%-10s' % status, status_color),
self.colored('%-10s' % arrival, 'blue'),
self.colored('%-20s' % obj.info, 'yellow'))
return result
class Parceloob(ReplApplication):
APPNAME = 'parceloob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2013 Romain Bignon'
CAPS = CapParcel
DESCRIPTION = "Console application to track your parcels."
SHORT_DESCRIPTION = "manage your parcels"
EXTRA_FORMATTERS = {'status': StatusFormatter,
'history': HistoryFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'status': 'status',
'info': 'history',
}
STORAGE = {'tracking': []}
def do_track(self, line):
"""
track ID
Track a parcel.
"""
parcel = self.get_object(line, 'get_parcel_tracking')
if not parcel:
print >>self.stderr, 'Error: the parcel "%s" is not found' % line
return 2
parcels = set(self.storage.get('tracking', default=[]))
parcels.add(parcel.fullid)
self.storage.set('tracking', list(parcels))
self.storage.save()
print 'Parcel "%s" has been tracked.' % parcel.fullid
def do_untrack(self, line):
"""
untrack ID
Stop tracking a parcel.
"""
removed = False
# Always try to first remove the parcel, the untrack should always success
parcels = set(self.storage.get('tracking', default=[]))
try:
parcels.remove(line)
removed = True
except KeyError:
pass
if not removed:
try:
parcel = self.get_object(line, 'get_parcel_tracking')
except ParcelNotFound:
parcel = False
if not parcel:
print >>self.stderr, 'Error: the parcel "%s" is not found. Did you provide the full id@backend parameter?' % line
return 2
try:
parcels.remove(parcel.fullid)
except KeyError:
print >>self.stderr, "Error: parcel \"%s\" wasn't tracked" % parcel.fullid
return 2
self.storage.set('tracking', list(parcels))
self.storage.save()
if removed:
print "Parcel \"%s\" isn't tracked anymore." % line
else:
print "Parcel \"%s\" isn't tracked anymore." % parcel.fullid
def do_status(self, line):
"""
status
Display status for all of the tracked parcels.
"""
backends = map(get_backend_name, self.enabled_backends)
self.start_format()
for id in self.storage.get('tracking', default=[]):
# It should be safe to do it here, since all objects in storage
# are stored with the fullid
_id, backend_name = id.rsplit('@', 1)
# If the user use the -b or -e option, do not try to get
# the status of parcel of not loaded backends
if backend_name not in backends:
continue
p = self.get_object(id, 'get_parcel_tracking')
if p is None:
continue
self.cached_format(p)
def do_info(self, id):
"""
info ID
Get information about a parcel.
"""
parcel = self.get_object(id, 'get_parcel_tracking', [])
if not parcel:
print >>self.stderr, 'Error: parcel not found'
return 2
self.start_format()
self.format(parcel)
for event in parcel.history:
self.format(event)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/pastoob/ 0000775 0000000 0000000 00000000000 12411220504 0025776 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/pastoob/__init__.py 0000664 0000000 0000000 00000001527 12411220504 0030114 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai
# Copyright(C) 2011 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .pastoob import Pastoob
__all__ = ['Pastoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/pastoob/pastoob.py 0000664 0000000 0000000 00000016516 12411220504 0030030 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011-2014 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import codecs
import re
from random import choice
from weboob.capabilities.paste import CapPaste, PasteNotFound
from weboob.tools.application.repl import ReplApplication
__all__ = ['Pastoob']
class Pastoob(ReplApplication):
APPNAME = 'pastoob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2011-2013 Laurent Bachelier'
DESCRIPTION = "Console application allowing to post and get pastes from pastebins."
SHORT_DESCRIPTION = "post and get pastes from pastebins"
CAPS = CapPaste
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def do_info(self, line):
"""
info ID [ID2 [...]]
Get information about pastes.
"""
if not line:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True)
return 2
self.start_format()
for _id in line.split(' '):
paste = self.get_object(_id, 'get_paste', ['id', 'title', 'language', 'public', 'contents'])
if not paste:
print >>self.stderr, 'Paste not found: %s' % _id
self.format(paste)
def do_get(self, line):
"""
get ID
Get a paste contents.
"""
return self._get_op(line, binary=False, command='get')
def do_get_bin(self, line):
"""
get_bin ID
Get a paste contents.
File will be downloaded from binary services.
"""
return self._get_op(line, binary=True, command='get_bin')
def _get_op(self, _id, binary, command='get'):
if not _id:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help(command, short=True)
return 2
try:
paste = self.get_object(_id, 'get_paste', ['contents'])
except PasteNotFound:
print >>self.stderr, 'Paste not found: %s' % _id
return 3
if not paste:
print >>self.stderr, 'Unable to handle paste: %s' % _id
return 1
if binary:
if self.interactive:
if not self.ask('The console may become messed up. Are you sure you want to show a binary file on your terminal?', default=False):
print >>self.stderr, 'Aborting.'
return 1
output = self.stdout
output.write(paste.contents.decode('base64'))
else:
output = codecs.getwriter(self.encoding)(self.stdout)
output.write(paste.contents)
# add a newline unless we are writing
# in a file or in a pipe
if os.isatty(output.fileno()):
output.write('\n')
def do_post(self, line):
"""
post [FILENAME]
Submit a new paste.
The filename can be '-' for reading standard input (pipe).
If 'bin' is passed, file will be uploaded to binary services.
"""
return self._post(line, binary=False)
def do_post_bin(self, line):
"""
post_bin [FILENAME]
Submit a new paste.
The filename can be '-' for reading standard input (pipe).
File will be uploaded to binary services.
"""
return self._post(line, binary=True)
def _post(self, filename, binary):
use_stdin = (not filename or filename == '-')
if use_stdin:
if binary:
contents = self.stdin.read()
else:
contents = self.acquire_input()
if not len(contents):
print >>self.stderr, 'Empty paste, aborting.'
return 1
else:
try:
if binary:
m = open(filename)
else:
m = codecs.open(filename, encoding=self.options.encoding or self.encoding)
with m as fp:
contents = fp.read()
except IOError as e:
print >>self.stderr, 'Unable to open file "%s": %s' % (filename, e.strerror)
return 1
if binary:
contents = contents.encode('base64')
# get and sort the backends able to satisfy our requirements
params = self.get_params()
backends = {}
for backend in self.weboob.iter_backends():
score = backend.can_post(contents, **params)
if score:
backends.setdefault(score, []).append(backend)
# select a random backend from the best scores
if len(backends):
backend = choice(backends[max(backends.keys())])
else:
print >>self.stderr, 'No suitable backend found.'
return 1
p = backend.new_paste(_id=None)
p.public = params['public']
if self.options.title is not None:
p.title = self.options.title
else:
p.title = os.path.basename(filename)
p.contents = contents
backend.post_paste(p, max_age=params['max_age'])
print 'Successfuly posted paste: %s' % p.page_url
def get_params(self):
return {'public': self.options.public,
'max_age': self.str_to_duration(self.options.max_age),
'title': self.options.title}
def str_to_duration(self, s):
if s.strip().lower() == 'never':
return False
parts = re.findall(r'(\d*(?:\.\d+)?)\s*([A-z]+)', s)
argsmap = {'Y|y|year|years|yr|yrs': 365.25 * 24 * 3600,
'M|o|month|months': 30.5 * 24 * 3600,
'W|w|week|weeks': 7 * 24 * 3600,
'D|d|day|days': 24 * 3600,
'H|h|hours|hour|hr|hrs': 3600,
'm|i|minute|minutes|min|mins': 60,
'S|s|second|seconds|sec|secs': 1}
seconds = 0
for number, unit in parts:
for rx, secs in argsmap.iteritems():
if re.match('^(%s)$' % rx, unit):
seconds += float(number) * float(secs)
return int(seconds)
def add_application_options(self, group):
group.add_option('-p', '--public', action='store_true',
help='Make paste public.')
group.add_option('-t', '--title', action='store',
help='Paste title',
type='string')
group.add_option('-m', '--max-age', action='store',
help='Maximum age (duration), default "1 month", "never" for infinite',
type='string', default='1 month')
group.add_option('-E', '--encoding', action='store',
help='Input encoding',
type='string')
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/ 0000775 0000000 0000000 00000000000 12411220504 0026140 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/__init__.py 0000664 0000000 0000000 00000000067 12411220504 0030254 0 ustar 00root root 0000000 0000000 from .qboobmsg import QBoobMsg
__all__ = ['QBoobMsg']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/main_window.py 0000664 0000000 0000000 00000003577 12411220504 0031041 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtCore import SIGNAL
from weboob.tools.application.qt import QtMainWindow
from weboob.tools.application.qt.backendcfg import BackendCfg
from weboob.capabilities.messages import CapMessages
from .ui.main_window_ui import Ui_MainWindow
from .messages_manager import MessagesManager
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, parent=None):
QtMainWindow.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.manager = MessagesManager(weboob, self)
self.setCentralWidget(self.manager)
self.connect(self.ui.actionBackends, SIGNAL("triggered()"), self.backendsConfig)
self.connect(self.ui.actionRefresh, SIGNAL("triggered()"), self.refresh)
if self.weboob.count_backends() == 0:
self.backendsConfig()
else:
self.centralWidget().load()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapMessages,), self)
if bckndcfg.run():
self.centralWidget().load()
def refresh(self):
self.centralWidget().refreshThreads()
messages_manager.py 0000664 0000000 0000000 00000024634 12411220504 0031745 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import time
import logging
from PyQt4.QtGui import QWidget, QTreeWidgetItem, QListWidgetItem, QMessageBox, QBrush
from PyQt4.QtCore import SIGNAL, Qt
from weboob.capabilities.messages import CapMessages, CapMessagesPost, Message
from weboob.tools.application.qt import QtDo
from weboob.tools.misc import to_unicode
from .ui.messages_manager_ui import Ui_MessagesManager
class MessagesManager(QWidget):
def __init__(self, weboob, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_MessagesManager()
self.ui.setupUi(self)
self.weboob = weboob
self.ui.backendsList.setCurrentRow(0)
self.backend = None
self.thread = None
self.message = None
self.ui.replyButton.setEnabled(False)
self.ui.replyWidget.hide()
self.connect(self.ui.backendsList, SIGNAL('itemSelectionChanged()'), self._backendChanged)
self.connect(self.ui.threadsList, SIGNAL('itemSelectionChanged()'), self._threadChanged)
self.connect(self.ui.messagesTree, SIGNAL('itemClicked(QTreeWidgetItem *, int)'), self._messageSelected)
self.connect(self.ui.messagesTree, SIGNAL('itemActivated(QTreeWidgetItem *, int)'), self._messageSelected)
self.connect(self.ui.profileButton, SIGNAL('clicked()'), self._profilePressed)
self.connect(self.ui.replyButton, SIGNAL('clicked()'), self._replyPressed)
self.connect(self.ui.sendButton, SIGNAL('clicked()'), self._sendPressed)
def load(self):
self.ui.backendsList.clear()
self.ui.backendsList.addItem('(All)')
for backend in self.weboob.iter_backends():
if not backend.has_caps(CapMessages):
continue
item = QListWidgetItem(backend.name.capitalize())
item.setData(Qt.UserRole, backend)
self.ui.backendsList.addItem(item)
self.refreshThreads()
def _backendChanged(self):
selection = self.ui.backendsList.selectedItems()
if not selection:
self.backend = None
return
self.backend = selection[0].data(Qt.UserRole).toPyObject()
self.refreshThreads()
def refreshThreads(self):
self.ui.messagesTree.clear()
self.ui.threadsList.clear()
self.hideReply()
self.ui.profileButton.hide()
self.ui.replyButton.setEnabled(False)
self.ui.backendsList.setEnabled(False)
self.ui.threadsList.setEnabled(False)
self.process_threads = QtDo(self.weboob, self._gotThread)
self.process_threads.do('iter_threads', backends=self.backend, caps=CapMessages)
def _gotThread(self, backend, thread):
if not backend:
self.process_threads = None
self.ui.backendsList.setEnabled(True)
self.ui.threadsList.setEnabled(True)
return
item = QListWidgetItem(thread.title)
item.setData(Qt.UserRole, (thread.backend, thread.id))
self.ui.threadsList.addItem(item)
def _threadChanged(self):
self.ui.messagesTree.clear()
selection = self.ui.threadsList.selectedItems()
if not selection:
return
t = selection[0].data(Qt.UserRole).toPyObject()
self.refreshThreadMessages(*t)
def refreshThreadMessages(self, backend, id):
self.ui.messagesTree.clear()
self.ui.messageBody.clear()
self.ui.backendsList.setEnabled(False)
self.ui.threadsList.setEnabled(False)
self.ui.replyButton.setEnabled(False)
self.ui.profileButton.hide()
self.hideReply()
self.process = QtDo(self.weboob, self._gotThreadMessages)
self.process.do('get_thread', id, backends=backend)
def _gotThreadMessages(self, backend, thread):
if thread is None:
self.ui.backendsList.setEnabled(True)
self.ui.threadsList.setEnabled(True)
self.process = None
return
self.thread = thread
if thread.flags & thread.IS_THREADS:
top = self.ui.messagesTree.invisibleRootItem()
else:
top = None
self._insert_message(thread.root, top)
self.showMessage(thread.root)
self.ui.messagesTree.expandAll()
def _insert_message(self, message, top):
item = QTreeWidgetItem(None, [message.title or '', message.sender or 'Unknown',
time.strftime('%Y-%m-%d %H:%M:%S', message.date.timetuple())])
item.setData(0, Qt.UserRole, message)
if message.flags & message.IS_UNREAD:
item.setForeground(0, QBrush(Qt.darkYellow))
item.setForeground(1, QBrush(Qt.darkYellow))
item.setForeground(2, QBrush(Qt.darkYellow))
if top is not None:
# threads
top.addChild(item)
else:
# discussion
self.ui.messagesTree.invisibleRootItem().insertChild(0, item)
if message.children is not None:
for child in message.children:
self._insert_message(child, top and item)
def _messageSelected(self, item, column):
message = item.data(0, Qt.UserRole).toPyObject()
self.showMessage(message, item)
def showMessage(self, message, item=None):
backend = self.weboob.get_backend(message.thread.backend)
if backend.has_caps(CapMessagesPost):
self.ui.replyButton.setEnabled(True)
self.message = message
if message.title.startswith('Re:'):
self.ui.titleEdit.setText(message.title)
else:
self.ui.titleEdit.setText('Re: %s' % message.title)
if message.flags & message.IS_HTML:
content = message.content
else:
content = message.content.replace('&', '&').replace('<', '<').replace('>', '>').replace('\n', ' ')
extra = u''
if message.flags & message.IS_NOT_RECEIVED:
extra += u'Status: Unread '
elif message.flags & message.IS_RECEIVED:
extra += u'Status: Read '
elif message.flags & message.IS_UNREAD:
extra += u'Status: New '
self.ui.messageBody.setText("
%s
"
"Date: %s "
"From: %s "
"%s"
"
%s
"
% (message.title, str(message.date), message.sender, extra, content))
if item and message.flags & message.IS_UNREAD:
backend.set_message_read(message)
message.flags &= ~message.IS_UNREAD
item.setForeground(0, QBrush())
item.setForeground(1, QBrush())
item.setForeground(2, QBrush())
if message.thread.flags & message.thread.IS_DISCUSSION:
self.ui.profileButton.show()
else:
self.ui.profileButton.hide()
def _profilePressed(self):
print self.thread.id
self.emit(SIGNAL('display_contact'), self.thread.id)
def displayReply(self):
self.ui.replyButton.setText(self.tr('Cancel'))
self.ui.replyWidget.show()
def hideReply(self):
self.ui.replyButton.setText(self.tr('Reply'))
self.ui.replyWidget.hide()
self.ui.replyEdit.clear()
self.ui.titleEdit.clear()
def _replyPressed(self):
if self.ui.replyWidget.isVisible():
self.hideReply()
else:
self.displayReply()
def _sendPressed(self):
if not self.ui.replyWidget.isVisible():
return
text = unicode(self.ui.replyEdit.toPlainText())
title = unicode(self.ui.titleEdit.text())
self.ui.backendsList.setEnabled(False)
self.ui.threadsList.setEnabled(False)
self.ui.messagesTree.setEnabled(False)
self.ui.replyButton.setEnabled(False)
self.ui.replyWidget.setEnabled(False)
self.ui.sendButton.setText(self.tr('Sending...'))
flags = 0
if self.ui.htmlBox.currentIndex() == 0:
flags = Message.IS_HTML
m = Message(thread=self.thread,
id=0,
title=title,
sender=None,
receivers=None,
content=text,
parent=self.message,
flags=flags)
self.process_reply = QtDo(self.weboob, self._postReply_cb, self._postReply_eb)
self.process_reply.do('post_message', m, backends=self.thread.backend)
def _postReply_cb(self, backend, ignored):
if not backend:
return
self.ui.backendsList.setEnabled(True)
self.ui.threadsList.setEnabled(True)
self.ui.messagesTree.setEnabled(True)
self.ui.replyButton.setEnabled(True)
self.ui.replyWidget.setEnabled(True)
self.ui.sendButton.setEnabled(True)
self.ui.sendButton.setText(self.tr('Send'))
self.hideReply()
self.process_reply = None
self.refreshThreadMessages(backend.name, self.thread.id)
def _postReply_eb(self, backend, error, backtrace):
content = unicode(self.tr('Unable to send message:\n%s\n')) % to_unicode(error)
if logging.root.level == logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while posting reply'),
content, QMessageBox.Ok)
self.ui.backendsList.setEnabled(True)
self.ui.threadsList.setEnabled(True)
self.ui.messagesTree.setEnabled(True)
self.ui.replyButton.setEnabled(True)
self.ui.replyWidget.setEnabled(True)
self.ui.sendButton.setText(self.tr('Send'))
self.process_reply = None
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/qboobmsg.py 0000664 0000000 0000000 00000002620 12411220504 0030323 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.messages import CapMessages
from weboob.tools.application.qt import QtApplication
from .main_window import MainWindow
class QBoobMsg(QtApplication):
APPNAME = 'qboobmsg'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Romain Bignon'
DESCRIPTION = "Qt application allowing to read messages on various websites and reply to them."
SHORT_DESCRIPTION = "send and receive message threads"
CAPS = CapMessages
def main(self, argv):
self.load_backends(CapMessages, storage=self.create_storage())
self.main_window = MainWindow(self.config, self.weboob)
self.main_window.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/ui/ 0000775 0000000 0000000 00000000000 12411220504 0026555 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/ui/Makefile 0000664 0000000 0000000 00000000265 12411220504 0030220 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/ui/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0030654 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/ui/main_window.ui 0000664 0000000 0000000 00000004306 12411220504 0031432 0 ustar 00root root 0000000 0000000
MainWindow00763580QBoobMsg0076320FiletoolBarTopToolBarAreafalseBackendsQuitQuitCtrl+QRefreshactionQuittriggered()MainWindowclose()-1-1381289
messages_manager.ui 0000664 0000000 0000000 00000021204 12411220504 0032335 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qboobmsg/ui
MessagesManager00696591Qt::Horizontal0015016777215Qt::HorizontalQt::Vertical0050QFrame::StyledPanelQFrame::Raised929200+00−Qt::Horizontal4020QAbstractItemView::NoEditTriggerstruetruetruetrue150trueTitleFromDateQt::Vertical05trueQt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouseQFrame::StyledPanelQFrame::RaisedProfileReply0With HTMLWithout HTMLSendexpandButtonclicked()messagesTreeexpandAll()73331527150collapseButtonclicked()messagesTreecollapseAll()73360527150
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ 0000775 0000000 0000000 00000000000 12411220504 0026126 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/__init__.py 0000664 0000000 0000000 00000000067 12411220504 0030242 0 ustar 00root root 0000000 0000000 from .qcineoob import QCineoob
__all__ = ['QCineoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/main_window.py 0000664 0000000 0000000 00000051671 12411220504 0031025 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import codecs
from PyQt4.QtCore import SIGNAL, Qt, QStringList
from PyQt4.QtGui import QApplication, QCompleter, QFrame, QShortcut, QKeySequence
from weboob.capabilities.cinema import CapCinema
from weboob.capabilities.torrent import CapTorrent
from weboob.capabilities.subtitle import CapSubtitle
from weboob.tools.application.qt import QtMainWindow, QtDo
from weboob.tools.application.qt.backendcfg import BackendCfg
from weboob.applications.suboob.suboob import LANGUAGE_CONV
from weboob.applications.qcineoob.ui.main_window_ui import Ui_MainWindow
from weboob.applications.qcineoob.ui.result_ui import Ui_Result
from .minimovie import MiniMovie
from .miniperson import MiniPerson
from .minitorrent import MiniTorrent
from .minisubtitle import MiniSubtitle
from .movie import Movie
from .person import Person
from .torrent import Torrent
from .subtitle import Subtitle
class Result(QFrame):
def __init__(self, weboob, app, parent=None):
QFrame.__init__(self, parent)
self.ui = Ui_Result()
self.ui.setupUi(self)
self.parent = parent
self.weboob = weboob
self.app = app
self.minis = []
self.current_info_widget = None
# action history is composed by the last action and the action list
# An action is a function, a list of arguments and a description string
self.action_history = {'last_action': None, 'action_list': []}
self.connect(self.ui.backButton, SIGNAL("clicked()"), self.doBack)
self.ui.backButton.hide()
def doAction(self, description, fun, args):
''' Call fun with args as arguments
and save it in the action history
'''
self.ui.currentActionLabel.setText(description)
if self.action_history['last_action'] is not None:
self.action_history['action_list'].append(self.action_history['last_action'])
self.ui.backButton.setToolTip(self.action_history['last_action']['description'])
self.ui.backButton.show()
self.action_history['last_action'] = {'function': fun, 'args': args, 'description': description}
return fun(*args)
def doBack(self):
''' Go back in action history
Basically call previous function and update history
'''
if len(self.action_history['action_list']) > 0:
todo = self.action_history['action_list'].pop()
self.ui.currentActionLabel.setText(todo['description'])
self.action_history['last_action'] = todo
if len(self.action_history['action_list']) == 0:
self.ui.backButton.hide()
else:
self.ui.backButton.setToolTip(self.action_history['action_list'][-1]['description'])
return todo['function'](*todo['args'])
def castingAction(self, backend_name, id, role):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
self.process = QtDo(self.weboob, self.addPerson)
self.process.do('iter_movie_persons', id, role, backends=backend_name, caps=CapCinema)
self.parent.ui.stopButton.show()
def filmographyAction(self, backend_name, id, role):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
self.process = QtDo(self.weboob, self.addMovie)
self.process.do('iter_person_movies', id, role, backends=backend_name, caps=CapCinema)
self.parent.ui.stopButton.show()
def search(self, tosearch, pattern, lang):
if tosearch == 'person':
self.searchPerson(pattern)
elif tosearch == 'movie':
self.searchMovie(pattern)
elif tosearch == 'torrent':
self.searchTorrent(pattern)
elif tosearch == 'subtitle':
self.searchSubtitle(lang, pattern)
def searchMovie(self, pattern):
if not pattern:
return
self.doAction(u'Search movie "%s"' % pattern, self.searchMovieAction, [pattern])
def searchMovieAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = str(self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex()).toString())
self.process = QtDo(self.weboob, self.addMovie)
#self.process.do('iter_movies', pattern, backends=backend_name, caps=CapCinema)
self.process.do(self.app._do_complete, self.parent.getCount(), ('original_title'), 'iter_movies', pattern, backends=backend_name, caps=CapCinema)
self.parent.ui.stopButton.show()
def stopProcess(self):
self.process.process.finish_event.set()
def addMovie(self, backend, movie):
if not backend:
self.parent.ui.searchEdit.setEnabled(True)
QApplication.restoreOverrideCursor()
self.process = None
self.parent.ui.stopButton.hide()
return
minimovie = MiniMovie(self.weboob, backend, movie, self)
self.ui.list_content.layout().addWidget(minimovie)
self.minis.append(minimovie)
def displayMovie(self, movie, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wmovie = Movie(movie, backend, self)
self.ui.info_content.layout().addWidget(wmovie)
self.current_info_widget = wmovie
QApplication.restoreOverrideCursor()
def searchPerson(self, pattern):
if not pattern:
return
self.doAction(u'Search person "%s"' % pattern, self.searchPersonAction, [pattern])
def searchPersonAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = str(self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex()).toString())
self.process = QtDo(self.weboob, self.addPerson)
#self.process.do('iter_persons', pattern, backends=backend_name, caps=CapCinema)
self.process.do(self.app._do_complete, self.parent.getCount(), ('name'), 'iter_persons', pattern, backends=backend_name, caps=CapCinema)
self.parent.ui.stopButton.show()
def addPerson(self, backend, person):
if not backend:
self.parent.ui.searchEdit.setEnabled(True)
QApplication.restoreOverrideCursor()
self.process = None
self.parent.ui.stopButton.hide()
return
miniperson = MiniPerson(self.weboob, backend, person, self)
self.ui.list_content.layout().addWidget(miniperson)
self.minis.append(miniperson)
def displayPerson(self, person, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wperson = Person(person, backend, self)
self.ui.info_content.layout().addWidget(wperson)
self.current_info_widget = wperson
QApplication.restoreOverrideCursor()
def searchTorrent(self, pattern):
if not pattern:
return
self.doAction(u'Search torrent "%s"' % pattern, self.searchTorrentAction, [pattern])
def searchTorrentAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = str(self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex()).toString())
self.process = QtDo(self.weboob, self.addTorrent)
#self.process.do('iter_torrents', pattern, backends=backend_name, caps=CapTorrent)
self.process.do(self.app._do_complete, self.parent.getCount(), ('name'), 'iter_torrents', pattern, backends=backend_name, caps=CapTorrent)
self.parent.ui.stopButton.show()
def addTorrent(self, backend, torrent):
if not backend:
self.parent.ui.searchEdit.setEnabled(True)
QApplication.restoreOverrideCursor()
self.process = None
self.parent.ui.stopButton.hide()
return
minitorrent = MiniTorrent(self.weboob, backend, torrent, self)
self.ui.list_content.layout().addWidget(minitorrent)
self.minis.append(minitorrent)
def displayTorrent(self, torrent, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wtorrent = Torrent(torrent, backend, self)
self.ui.info_content.layout().addWidget(wtorrent)
self.current_info_widget = wtorrent
def searchSubtitle(self, lang, pattern):
if not pattern:
return
self.doAction(u'Search subtitle "%s" (lang:%s)' % (pattern, lang), self.searchSubtitleAction, [lang, pattern])
def searchSubtitleAction(self, lang, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.parent.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = str(self.parent.ui.backendEdit.itemData(self.parent.ui.backendEdit.currentIndex()).toString())
self.process = QtDo(self.weboob, self.addSubtitle)
#self.process.do('iter_subtitles', lang, pattern, backends=backend_name, caps=CapSubtitle)
self.process.do(self.app._do_complete, self.parent.getCount(), ('name'), 'iter_subtitles', lang, pattern, backends=backend_name, caps=CapSubtitle)
self.parent.ui.stopButton.show()
def addSubtitle(self, backend, subtitle):
if not backend:
self.parent.ui.searchEdit.setEnabled(True)
QApplication.restoreOverrideCursor()
self.process = None
self.parent.ui.stopButton.hide()
return
minisubtitle = MiniSubtitle(self.weboob, backend, subtitle, self)
self.ui.list_content.layout().addWidget(minisubtitle)
self.minis.append(minisubtitle)
def displaySubtitle(self, subtitle, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wsubtitle = Subtitle(subtitle, backend, self)
self.ui.info_content.layout().addWidget(wsubtitle)
self.current_info_widget = wsubtitle
def searchId(self, id, stype):
QApplication.setOverrideCursor(Qt.WaitCursor)
title_field = 'name'
if stype == 'movie':
cap = CapCinema
title_field = 'original_title'
elif stype == 'person':
cap = CapCinema
elif stype == 'torrent':
cap = CapTorrent
elif stype == 'subtitle':
cap = CapSubtitle
if '@' in id:
backend_name = id.split('@')[1]
id = id.split('@')[0]
else:
backend_name = None
for backend in self.weboob.iter_backends():
if backend.has_caps(cap) and ((backend_name and backend.name == backend_name) or not backend_name):
exec('object = backend.get_%s(id)' % (stype))
if object:
func_display = 'self.display' + stype[0].upper() + stype[1:]
exec("self.doAction('Details of %s \"%%s\"' %% object.%s, %s, [object, backend])" %
(stype, title_field, func_display))
QApplication.restoreOverrideCursor()
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
QtMainWindow.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.app = app
# search history is a list of patterns which have been searched
self.search_history = self.loadSearchHistory()
self.updateCompletion()
self.connect(self.ui.searchEdit, SIGNAL("returnPressed()"), self.search)
self.connect(self.ui.idEdit, SIGNAL("returnPressed()"), self.searchId)
self.connect(self.ui.typeCombo, SIGNAL("currentIndexChanged(QString)"), self.typeComboChanged)
count = self.config.get('settings', 'maxresultsnumber')
self.ui.countSpin.setValue(int(count))
showT = self.config.get('settings', 'showthumbnails')
self.ui.showTCheck.setChecked(showT == '1')
self.connect(self.ui.stopButton, SIGNAL("clicked()"), self.stopProcess)
self.ui.stopButton.hide()
self.connect(self.ui.actionBackends, SIGNAL("triggered()"), self.backendsConfig)
q = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_Q), self)
self.connect(q, SIGNAL("activated()"), self.close)
n = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_PageDown), self)
self.connect(n, SIGNAL("activated()"), self.nextTab)
p = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_PageUp), self)
self.connect(p, SIGNAL("activated()"), self.prevTab)
w = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_W), self)
self.connect(w, SIGNAL("activated()"), self.closeCurrentTab)
l = QShortcut(QKeySequence(Qt.CTRL + Qt.Key_L), self)
self.connect(l, SIGNAL("activated()"), self.ui.searchEdit.setFocus)
self.connect(l, SIGNAL("activated()"), self.ui.searchEdit.selectAll)
self.connect(self.ui.resultsTab, SIGNAL("tabCloseRequested(int)"), self.closeTab)
self.loadBackendsList()
if self.ui.backendEdit.count() == 0:
self.backendsConfig()
langs = sorted(LANGUAGE_CONV.keys())
for lang in langs:
self.ui.langCombo.addItem(lang)
self.ui.langCombo.hide()
self.ui.langLabel.hide()
def stopProcess(self):
self.ui.resultsTab.currentWidget().stopProcess()
def closeTab(self, index):
if self.ui.resultsTab.widget(index) != 0:
tabToClose = self.ui.resultsTab.widget(index)
self.ui.resultsTab.removeTab(index)
del(tabToClose)
def closeCurrentTab(self):
self.closeTab(self.ui.resultsTab.currentIndex())
def prevTab(self):
index = self.ui.resultsTab.currentIndex() - 1
size = self.ui.resultsTab.count()
if size != 0:
self.ui.resultsTab.setCurrentIndex(index % size)
def nextTab(self):
index = self.ui.resultsTab.currentIndex() + 1
size = self.ui.resultsTab.count()
if size != 0:
self.ui.resultsTab.setCurrentIndex(index % size)
def newTab(self, txt, backend, person=None, movie=None, torrent=None, subtitle=None):
id = ''
if person is not None:
id = person.id
stype = 'person'
elif movie is not None:
id = movie.id
stype = 'movie'
elif subtitle is not None:
id = subtitle.id
stype = 'subtitle'
elif torrent is not None:
id = torrent.id
stype = 'torrent'
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, txt)
new_res.searchId(id, stype)
def search(self):
pattern = unicode(self.ui.searchEdit.text())
# arbitrary max number of completion word
if len(self.search_history) > 50:
self.search_history.pop(0)
if pattern not in self.search_history:
self.search_history.append(pattern)
self.updateCompletion()
tosearch = unicode(self.ui.typeCombo.currentText())
lang = unicode(self.ui.langCombo.currentText())
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, pattern)
self.ui.resultsTab.setCurrentWidget(new_res)
new_res.search(tosearch, pattern, lang)
def searchId(self):
id = unicode(self.ui.idEdit.text())
stype = unicode(self.ui.idTypeCombo.currentText())
new_res = Result(self.weboob, self.app, self)
self.ui.resultsTab.addTab(new_res, id)
self.ui.resultsTab.setCurrentWidget(new_res)
new_res.searchId(id, stype)
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapCinema, CapTorrent, CapSubtitle,), self)
if bckndcfg.run():
self.loadBackendsList()
def loadBackendsList(self):
self.ui.backendEdit.clear()
for i, backend in enumerate(self.weboob.iter_backends()):
if i == 0:
self.ui.backendEdit.addItem('All backends', '')
self.ui.backendEdit.addItem(backend.name, backend.name)
if backend.name == self.config.get('settings', 'backend'):
self.ui.backendEdit.setCurrentIndex(i+1)
if self.ui.backendEdit.count() == 0:
self.ui.searchEdit.setEnabled(False)
else:
self.ui.searchEdit.setEnabled(True)
def loadSearchHistory(self):
''' Return search string history list loaded from history file
'''
result = []
history_path = os.path.join(self.weboob.workdir, 'qcineoob_history')
if os.path.exists(history_path):
f = codecs.open(history_path, 'r', 'utf-8')
conf_hist = f.read()
f.close()
if conf_hist is not None and conf_hist.strip() != '':
result = conf_hist.strip().split('\n')
return result
def saveSearchHistory(self):
''' Save search history in history file
'''
if len(self.search_history) > 0:
history_path = os.path.join(self.weboob.workdir, 'qcineoob_history')
f = codecs.open(history_path, 'w', 'utf-8')
f.write('\n'.join(self.search_history))
f.close()
def updateCompletion(self):
qc = QCompleter(QStringList(self.search_history), self)
qc.setCaseSensitivity(Qt.CaseInsensitive)
self.ui.searchEdit.setCompleter(qc)
def typeComboChanged(self, value):
if unicode(value) == 'subtitle':
self.ui.langCombo.show()
self.ui.langLabel.show()
else:
self.ui.langCombo.hide()
self.ui.langLabel.hide()
def getCount(self):
num = self.ui.countSpin.value()
if num == 0:
return None
else:
return num
def closeEvent(self, ev):
self.config.set('settings', 'backend', str(self.ui.backendEdit.itemData(
self.ui.backendEdit.currentIndex()).toString()))
self.saveSearchHistory()
self.config.set('settings', 'maxresultsnumber', self.ui.countSpin.value())
self.config.set('settings', 'showthumbnails', '1' if self.ui.showTCheck.isChecked() else '0')
self.config.save()
ev.accept()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/minimovie.py 0000664 0000000 0000000 00000005530 12411220504 0030477 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import urllib
from PyQt4.QtGui import QFrame, QImage, QPixmap, QApplication
from PyQt4.QtCore import Qt
from weboob.applications.qcineoob.ui.minimovie_ui import Ui_MiniMovie
from weboob.capabilities.base import empty, NotAvailable
class MiniMovie(QFrame):
def __init__(self, weboob, backend, movie, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.ui = Ui_MiniMovie()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.movie = movie
self.ui.titleLabel.setText(movie.original_title)
self.ui.shortDescLabel.setText(movie.short_description)
self.ui.backendLabel.setText(backend.name)
if self.parent.parent.ui.showTCheck.isChecked():
self.gotThumbnail()
def gotThumbnail(self):
if empty(self.movie.thumbnail_url) and self.movie.thumbnail_url != NotAvailable:
self.backend.fill_movie(self.movie, ('thumbnail_url'))
if not empty(self.movie.thumbnail_url):
data = urllib.urlopen(self.movie.thumbnail_url).read()
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToHeight(100,Qt.SmoothTransformation))
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
if event.button() == 2:
self.gotThumbnail()
elif event.button() == 4:
movie = self.backend.get_movie(self.movie.id)
self.parent.parent.newTab(u'Details of movie "%s"' %
movie.original_title, self.backend, movie=movie)
else:
QApplication.setOverrideCursor(Qt.WaitCursor)
movie = self.backend.get_movie(self.movie.id)
if movie:
self.parent.doAction('Details of movie "%s"' %
movie.original_title, self.parent.displayMovie, [movie, self.backend])
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/miniperson.py 0000664 0000000 0000000 00000006224 12411220504 0030667 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import urllib
from PyQt4.QtGui import QFrame, QImage, QPixmap, QApplication
from PyQt4.QtCore import Qt
from weboob.applications.qcineoob.ui.miniperson_ui import Ui_MiniPerson
from weboob.capabilities.base import empty, NotAvailable
class MiniPerson(QFrame):
def __init__(self, weboob, backend, person, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.ui = Ui_MiniPerson()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.person = person
self.ui.nameLabel.setText('%s' % person.name)
if not empty(person.short_description):
if unicode(self.parent.ui.currentActionLabel.text()).startswith('Casting'):
self.ui.shortDescTitleLabel.setText(u'Role')
self.ui.shortDescLabel.setText('%s' % person.short_description)
else:
self.ui.shortDescTitleLabel.hide()
self.ui.shortDescLabel.hide()
self.ui.backendLabel.setText(backend.name)
if self.parent.parent.ui.showTCheck.isChecked():
self.gotThumbnail()
def gotThumbnail(self):
if empty(self.person.thumbnail_url) and self.person.thumbnail_url != NotAvailable:
self.backend.fill_person(self.person, ('thumbnail_url'))
if not empty(self.person.thumbnail_url):
data = urllib.urlopen(self.person.thumbnail_url).read()
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToHeight(100,Qt.SmoothTransformation))
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
if event.button() == 2:
self.gotThumbnail()
if event.button() == 4:
person = self.backend.get_person(self.person.id)
self.parent.parent.newTab(u'Details of person "%s"' %
person.name, self.backend, person=person)
else:
QApplication.setOverrideCursor(Qt.WaitCursor)
person = self.backend.get_person(self.person.id)
if person:
self.parent.doAction(u'Details of person "%s"' %
person.name, self.parent.displayPerson, [person, self.backend])
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/minisubtitle.py 0000664 0000000 0000000 00000004154 12411220504 0031214 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QFrame
from weboob.applications.qcineoob.ui.minisubtitle_ui import Ui_MiniSubtitle
from weboob.capabilities.base import empty
class MiniSubtitle(QFrame):
def __init__(self, weboob, backend, subtitle, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.ui = Ui_MiniSubtitle()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.subtitle = subtitle
self.ui.nameLabel.setText(subtitle.name)
if not empty(subtitle.nb_cd):
self.ui.nbcdLabel.setText(u'%s' % subtitle.nb_cd)
self.ui.backendLabel.setText(backend.name)
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
subtitle = self.backend.get_subtitle(self.subtitle.id)
if event.button() == 4:
self.parent.parent.newTab(u'Details of subtitle "%s"' %
subtitle.name, self.backend, subtitle=subtitle)
else:
if subtitle:
self.parent.doAction('Details of subtitle "%s"' %
subtitle.name, self.parent.displaySubtitle, [subtitle, self.backend])
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/minitorrent.py 0000664 0000000 0000000 00000004504 12411220504 0031055 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QFrame
from weboob.applications.qcineoob.ui.minitorrent_ui import Ui_MiniTorrent
from weboob.applications.weboorrents.weboorrents import sizeof_fmt
from weboob.capabilities.base import empty
class MiniTorrent(QFrame):
def __init__(self, weboob, backend, torrent, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.ui = Ui_MiniTorrent()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.torrent = torrent
self.ui.nameLabel.setText(torrent.name)
if not empty(torrent.seeders) and not empty(torrent.leechers):
self.ui.seedLeechLabel.setText('%s/%s' % (torrent.seeders, torrent.leechers))
if not empty(torrent.size):
self.ui.sizeLabel.setText(u'%s' % sizeof_fmt(torrent.size))
self.ui.backendLabel.setText(backend.name)
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
torrent = self.backend.get_torrent(self.torrent.id)
if event.button() == 4:
self.parent.parent.newTab(u'Details of torrent "%s"' %
torrent.name, self.backend, torrent=torrent)
else:
if torrent:
self.parent.doAction('Details of torrent "%s"' %
torrent.name, self.parent.displayTorrent, [torrent, self.backend])
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/movie.py 0000664 0000000 0000000 00000011534 12411220504 0027623 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import urllib
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.QtGui import QFrame, QImage, QPixmap
from weboob.applications.qcineoob.ui.movie_ui import Ui_Movie
from weboob.capabilities.base import empty
from weboob.applications.suboob.suboob import LANGUAGE_CONV
class Movie(QFrame):
def __init__(self, movie, backend, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.ui = Ui_Movie()
self.ui.setupUi(self)
langs = sorted(LANGUAGE_CONV.keys())
for lang in langs:
self.ui.langCombo.addItem(lang)
self.connect(self.ui.castingButton, SIGNAL("clicked()"), self.casting)
self.connect(self.ui.torrentButton, SIGNAL("clicked()"), self.searchTorrent)
self.connect(self.ui.subtitleButton, SIGNAL("clicked()"), self.searchSubtitle)
self.movie = movie
self.backend = backend
self.ui.titleLabel.setText(movie.original_title)
self.ui.durationLabel.setText(unicode(movie.duration))
self.gotThumbnail()
self.putReleases()
self.ui.idEdit.setText(u'%s@%s' % (movie.id, backend.name))
if not empty(movie.other_titles):
self.ui.otherTitlesPlain.setPlainText('\n'.join(movie.other_titles))
else:
self.ui.otherTitlesPlain.parent().hide()
if not empty(movie.genres):
genres = u''
for g in movie.genres:
genres += '%s, ' % g
genres = genres[:-2]
self.ui.genresLabel.setText(genres)
else:
self.ui.genresLabel.parent().hide()
if not empty(movie.release_date):
self.ui.releaseDateLabel.setText(movie.release_date.strftime('%Y-%m-%d'))
else:
self.ui.releaseDateLabel.parent().hide()
if not empty(movie.duration):
self.ui.durationLabel.setText('%s min' % movie.duration)
else:
self.ui.durationLabel.parent().hide()
if not empty(movie.pitch):
self.ui.pitchPlain.setPlainText('%s' % movie.pitch)
else:
self.ui.pitchPlain.parent().hide()
if not empty(movie.country):
self.ui.countryLabel.setText('%s' % movie.country)
else:
self.ui.countryLabel.parent().hide()
if not empty(movie.note):
self.ui.noteLabel.setText('%s' % movie.note)
else:
self.ui.noteLabel.parent().hide()
for role in movie.roles.keys():
self.ui.castingCombo.addItem('%s' % role)
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
self.ui.verticalLayout_2.setAlignment(Qt.AlignTop)
def putReleases(self):
rel = self.backend.get_movie_releases(self.movie.id)
if not empty(rel):
self.ui.allReleasesPlain.setPlainText(rel)
else:
self.ui.allReleasesPlain.parent().hide()
def gotThumbnail(self):
if not empty(self.movie.thumbnail_url):
data = urllib.urlopen(self.movie.thumbnail_url).read()
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToWidth(220,Qt.SmoothTransformation))
def searchSubtitle(self):
tosearch = unicode(self.movie.original_title)
lang = unicode(self.ui.langCombo.currentText())
desc = 'Search subtitles for "%s" (lang:%s)' % (tosearch, lang)
self.parent.doAction(desc, self.parent.searchSubtitleAction, [lang, tosearch])
def searchTorrent(self):
tosearch = self.movie.original_title
if not empty(self.movie.release_date):
tosearch += ' %s' % self.movie.release_date.year
desc = 'Search torrents for "%s"' % tosearch
self.parent.doAction(desc, self.parent.searchTorrentAction, [tosearch])
def casting(self):
role = None
tosearch = unicode(self.ui.castingCombo.currentText())
role_desc = ''
if tosearch != 'all':
role = tosearch
role_desc = ' as %s' % role
self.parent.doAction('Casting%s of movie "%s"' % (role_desc, self.movie.original_title),
self.parent.castingAction, [self.backend.name, self.movie.id, role])
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/person.py 0000664 0000000 0000000 00000006774 12411220504 0030024 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import urllib
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QFrame, QImage, QPixmap, QApplication
from weboob.applications.qcineoob.ui.person_ui import Ui_Person
from weboob.capabilities.base import empty
class Person(QFrame):
def __init__(self, person, backend, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.ui = Ui_Person()
self.ui.setupUi(self)
self.connect(self.ui.filmographyButton, SIGNAL("clicked()"), self.filmography)
self.connect(self.ui.biographyButton, SIGNAL("clicked()"), self.biography)
self.person = person
self.backend = backend
self.gotThumbnail()
self.ui.nameLabel.setText(person.name)
self.ui.idEdit.setText(u'%s@%s' % (person.id, backend.name))
if not empty(person.real_name):
self.ui.realNameLabel.setText('%s' % person.real_name)
else:
self.ui.realNameLabel.parent().hide()
if not empty(person.birth_place):
self.ui.birthPlaceLabel.setText('%s' % person.birth_place)
else:
self.ui.birthPlaceLabel.parent().hide()
if not empty(person.birth_date):
self.ui.birthDateLabel.setText(person.birth_date.strftime('%Y-%m-%d'))
else:
self.ui.birthDateLabel.parent().hide()
if not empty(person.death_date):
self.ui.deathDateLabel.setText(person.death_date.strftime('%Y-%m-%d'))
else:
self.ui.deathDateLabel.parent().hide()
self.ui.shortBioPlain.setPlainText('%s' % person.short_biography)
for role in person.roles.keys():
self.ui.filmographyCombo.addItem(role)
self.ui.verticalLayout_2.setAlignment(Qt.AlignTop)
def gotThumbnail(self):
if not empty(self.person.thumbnail_url):
data = urllib.urlopen(self.person.thumbnail_url).read()
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToWidth(220,Qt.SmoothTransformation))
def filmography(self):
role = None
tosearch = unicode(self.ui.filmographyCombo.currentText())
role_desc = ''
if tosearch != 'all':
role = tosearch
role_desc = ' as %s' % role
self.parent.doAction('Filmography of "%s"%s' % (self.person.name, role_desc),
self.parent.filmographyAction, [self.backend.name, self.person.id, role])
def biography(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
self.backend.fill_person(self.person, 'biography')
bio = self.person.biography
self.ui.shortBioPlain.setPlainText(u'%s' % bio)
self.ui.biographyLabel.setText('Full biography:')
self.ui.biographyButton.hide()
QApplication.restoreOverrideCursor()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/qcineoob.py 0000664 0000000 0000000 00000003342 12411220504 0030301 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.cinema import CapCinema
from weboob.capabilities.torrent import CapTorrent
from weboob.capabilities.subtitle import CapSubtitle
from weboob.tools.application.qt import QtApplication
from .main_window import MainWindow
class QCineoob(QtApplication):
APPNAME = 'qcineoob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2013 Julien Veyssier'
DESCRIPTION = "Qt application allowing to search movies, people, torrent and subtitles."
SHORT_DESCRIPTION = "search movies, people, torrent and subtitles"
CAPS = CapCinema, CapTorrent, CapSubtitle
CONFIG = {'settings': {'backend': '',
'maxresultsnumber': '10',
'showthumbnails': '0'
}
}
def main(self, argv):
self.load_backends([CapCinema, CapTorrent, CapSubtitle])
self.load_config()
self.main_window = MainWindow(self.config, self.weboob, self)
self.main_window.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/subtitle.py 0000664 0000000 0000000 00000006465 12411220504 0030346 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.QtGui import QFrame, QFileDialog
from weboob.applications.qcineoob.ui.subtitle_ui import Ui_Subtitle
from weboob.capabilities.base import empty
class Subtitle(QFrame):
def __init__(self, subtitle, backend, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.backend = backend
self.ui = Ui_Subtitle()
self.ui.setupUi(self)
self.connect(self.ui.downloadButton, SIGNAL("clicked()"), self.download)
self.subtitle = subtitle
self.ui.idEdit.setText(u'%s@%s' % (subtitle.id, backend.name))
self.ui.nameLabel.setText(u'%s' % subtitle.name)
if not empty(subtitle.nb_cd):
self.ui.nbcdLabel.setText(u'%s' % subtitle.nb_cd)
else:
self.ui.nbcdLabel.parent().hide()
if not empty(subtitle.language):
self.ui.langLabel.setText(u'%s' % subtitle.language)
else:
self.ui.langLabel.parent().hide()
if not empty(subtitle.description):
self.ui.descriptionPlain.setPlainText(u'%s' % subtitle.description)
else:
self.ui.descriptionPlain.parent().hide()
if not empty(subtitle.url):
self.ui.urlEdit.setText(u'%s' % subtitle.url)
else:
self.ui.downloadButton.setDisabled(True)
self.ui.downloadButton.setText('Impossible to download this subtitle')
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
def download(self):
if not empty(self.subtitle.url):
if self.subtitle.url.endswith('.rar'):
ext = '.rar'
elif self.subtitle.url.endswith('.srt'):
ext = '.srt'
else:
ext = '.zip'
fileDial = QFileDialog(self, 'Save "%s" subtitle file' %
self.subtitle.name, '%s%s' % (self.subtitle.name, ext), 'all files (*)')
fileDial.setAcceptMode(QFileDialog.AcceptSave)
fileDial.setLabelText(QFileDialog.Accept, 'Save subtitle file')
fileDial.setLabelText(QFileDialog.FileName, 'Subtitle file name')
ok = (fileDial.exec_() == 1)
if not ok:
return
result = fileDial.selectedFiles()
if len(result) > 0:
dest = result[0]
data = self.backend.get_subtitle_file(self.subtitle.id)
try:
with open(dest, 'w') as f:
f.write(data)
except IOError as e:
print >>self.stderr, 'Unable to write subtitle file in "%s": %s' % (dest, e)
return 1
return
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/torrent.py 0000664 0000000 0000000 00000007205 12411220504 0030201 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.QtGui import QFrame, QFileDialog
from weboob.applications.qcineoob.ui.torrent_ui import Ui_Torrent
from weboob.applications.weboorrents.weboorrents import sizeof_fmt
from weboob.capabilities.base import empty
class Torrent(QFrame):
def __init__(self, torrent, backend, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.backend = backend
self.ui = Ui_Torrent()
self.ui.setupUi(self)
self.connect(self.ui.downloadButton, SIGNAL("clicked()"), self.download)
self.torrent = torrent
self.ui.idEdit.setText(u'%s@%s' % (torrent.id, backend.name))
self.ui.nameLabel.setText(u'%s' % torrent.name)
if not empty(torrent.url):
self.ui.urlEdit.setText(u'%s' % torrent.url)
else:
self.ui.urlFrame.hide()
self.ui.downloadButton.setDisabled(True)
if not empty(torrent.magnet):
self.ui.downloadButton.setText(u'Download not available\nbut magnet link provided')
self.ui.downloadButton.setToolTip(u'Use the magnet link')
if not empty(torrent.description):
self.ui.descriptionPlain.setText(u'%s' % torrent.description)
else:
self.ui.descriptionPlain.parent().hide()
if not empty(torrent.files):
files = u''
for f in torrent.files:
files += '%s\n' % f
self.ui.filesPlain.setText(u'%s' % files)
else:
self.ui.filesPlain.parent().hide()
if not empty(torrent.magnet):
self.ui.magnetEdit.setText(u'%s' % torrent.magnet)
else:
self.ui.magnetFrame.hide()
if not empty(torrent.seeders) and not empty(torrent.leechers):
self.ui.seedLeechLabel.setText(u'%s/%s' % (torrent.seeders, torrent.leechers))
if not empty(torrent.size):
self.ui.sizeLabel.setText(u'%s' % sizeof_fmt(torrent.size))
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
def download(self):
fileDial = QFileDialog(self, 'Save "%s" torrent file' % self.torrent.name, '%s.torrent' %
self.torrent.name, 'Torrent file (*.torrent);;all files (*)')
fileDial.setAcceptMode(QFileDialog.AcceptSave)
fileDial.setLabelText(QFileDialog.Accept, 'Save torrent file')
fileDial.setLabelText(QFileDialog.FileName, 'Torrent file name')
ok = (fileDial.exec_() == 1)
if not ok:
return
result = fileDial.selectedFiles()
if len(result) > 0:
dest = result[0]
data = self.backend.get_torrent_file(self.torrent.id)
try:
with open(unicode(dest), 'w') as f:
f.write(data)
except IOError as e:
print >>self.stderr, 'Unable to write .torrent in "%s": %s' % (dest, e)
return 1
return
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/ 0000775 0000000 0000000 00000000000 12411220504 0026543 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/Makefile 0000664 0000000 0000000 00000000265 12411220504 0030206 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0030642 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/main_window.ui 0000664 0000000 0000000 00000013714 12411220504 0031423 0 ustar 00root root 0000000 0000000
MainWindow00748463QCineoob4QFrame::StyledPanelQFrame::Raised00Search: 451677721575truebackground-color: qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.479904 rgba(255, 0, 0, 255), stop:0.522685 rgba(255, 255, 255, 255), stop:1 rgba(255, 255, 255, 255));stopmoviepersontorrentsubtitle8language<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>max results<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>-1truetrueShow thumbnailsSearch by id:moviepersontorrentsubtitle0074823toolBarTopToolBarAreafalseBackends
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/minimovie.ui 0000664 0000000 0000000 00000006604 12411220504 0031104 0 ustar 00root root 0000000 0000000
MiniMovie00464136FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueTitle0050truefalseTextLabeltrue75trueShort description:TextLabelfalse75trueWhereTextLabel
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/miniperson.ui 0000664 0000000 0000000 00000006617 12411220504 0031277 0 ustar 00root root 0000000 0000000
MiniPerson00464136FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueName0050truefalseTextLabeltrue75trueShort descriptionTextLabelfalse75trueWhereTextLabel
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/minisubtitle.ui0000664 0000000 0000000 00000006446 12411220504 0031624 0 ustar 00root root 0000000 0000000
MiniSubtitle00464136FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueName0050truefalseTextLabeltrue75trueNb CDTextLabel75trueWhereTextLabel
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/minitorrent.ui 0000664 0000000 0000000 00000007340 12411220504 0031460 0 ustar 00root root 0000000 0000000
MiniTorrent00464136FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueName0050truefalseTextLabel75trueSeed/leech:TextLabel75trueWhereTextLabelTextLabel75trueSize:
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/movie.ui 0000664 0000000 0000000 00000040006 12411220504 0030221 0 ustar 00root root 0000000 0000000
Movie00645600001000010000FrameQFrame::StyledPanelQFrame::Raised167772153000QFrame::NoFrameQFrame::RaisedQFrame::NoFrameQFrame::Raised00view castingallQFrame::NoFrameQFrame::Raised00search torrentsQFrame::NoFrameQFrame::Raised00search subtitles167772159000QFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue1677721535QFrame::StyledPanelQFrame::Raised75trueTitle:1677721535QFrame::StyledPanelQFrame::Raised75trueDuration:1677721535QFrame::StyledPanelQFrame::Raised75trueRelease date:16777215150QFrame::StyledPanelQFrame::Raised75truePitch:00Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse1677721535QFrame::StyledPanelQFrame::Raised75trueCountry:1677721535QFrame::StyledPanelQFrame::Raised75trueGenres:true1677721535QFrame::StyledPanelQFrame::Raised75trueNote:16777215200QFrame::StyledPanelQFrame::Raised75trueAll release dates:Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse0016777215200QFrame::StyledPanelQFrame::Raised1001677721575trueOther titles:Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/person.ui 0000664 0000000 0000000 00000023705 12411220504 0030417 0 ustar 00root root 0000000 0000000
Person00857629FrameQFrame::StyledPanelQFrame::RaisedQFrame::NoFrameQFrame::RaisedQFrame::NoFrameQFrame::Raisedview
filmographyasallQFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrueQFrame::StyledPanelQFrame::Raised75truename:QFrame::StyledPanelQFrame::Raised75trueBirth date:QFrame::StyledPanelQFrame::Raised75trueReal Name:QFrame::StyledPanelQFrame::Raised75trueBirth place:QFrame::StyledPanelQFrame::Raised75trueDeath date:QFrame::StyledPanelQFrame::RaisedQFrame::NoFrameQFrame::Raised75trueShort biography:view full
biography16777215100Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/result.ui 0000664 0000000 0000000 00000010330 12411220504 0030415 0 ustar 00root root 0000000 0000000
Result00645733001000010000FrameQFrame::StyledPanelQFrame::RaisedQFrame::NoFrameQFrame::Raised00Qt::Horizontal40206020<<back75trueQt::AlignCentertrueQt::Horizontal4020true00601663true009626
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/subtitle.ui 0000664 0000000 0000000 00000020436 12411220504 0030742 0 ustar 00root root 0000000 0000000
Subtitle00629552002000600FrameQFrame::StyledPanelQFrame::Raised16777215600QFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue167772151000QFrame::StyledPanelQFrame::Raised75trueName:true167772151000QFrame::StyledPanelQFrame::Raised75trueNb CD:167772151000QFrame::StyledPanelQFrame::Raised75trueLang:16777215200QFrame::StyledPanelQFrame::Raised75trueDescription:167772151201677721540QFrame::StyledPanelQFrame::Raised3375trueUrl:Download
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcineoob/ui/torrent.ui 0000664 0000000 0000000 00000024562 12411220504 0030610 0 ustar 00root root 0000000 0000000
Torrent00629571001000010000FrameQFrame::StyledPanelQFrame::Raised1677721510000QFrame::NoFrameQFrame::Raised1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue1677721535QFrame::StyledPanelQFrame::Raised75trueName:1677721535QFrame::StyledPanelQFrame::Raised75trueSeed/leech:1677721535QFrame::StyledPanelQFrame::Raised75trueSize:015016777215500QFrame::StyledPanelQFrame::Raised75trueDescription:012016777215200QFrame::StyledPanelQFrame::Raised75trueFiles:1677721540QFrame::StyledPanelQFrame::Raised3375trueUrl:1677721540QFrame::StyledPanelQFrame::Raised3375trueMagnet:Download
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/ 0000775 0000000 0000000 00000000000 12411220504 0026305 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/__init__.py 0000664 0000000 0000000 00000000072 12411220504 0030415 0 ustar 00root root 0000000 0000000 from .qcookboob import QCookboob
__all__ = ['QCookboob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/main_window.py 0000664 0000000 0000000 00000021712 12411220504 0031175 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import codecs
from PyQt4.QtCore import SIGNAL, Qt, QStringList
from PyQt4.QtGui import QApplication, QCompleter
from weboob.capabilities.recipe import CapRecipe
from weboob.tools.application.qt import QtMainWindow, QtDo
from weboob.tools.application.qt.backendcfg import BackendCfg
from weboob.applications.qcookboob.ui.main_window_ui import Ui_MainWindow
from .minirecipe import MiniRecipe
from .recipe import Recipe
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
QtMainWindow.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.app = app
self.minis = []
self.current_info_widget = None
# search history is a list of patterns which have been searched
self.search_history = self.loadSearchHistory()
self.updateCompletion()
# action history is composed by the last action and the action list
# An action is a function, a list of arguments and a description string
self.action_history = {'last_action': None, 'action_list': []}
self.connect(self.ui.backButton, SIGNAL("clicked()"), self.doBack)
self.ui.backButton.hide()
self.connect(self.ui.stopButton, SIGNAL("clicked()"), self.stopProcess)
self.ui.stopButton.hide()
self.connect(self.ui.searchEdit, SIGNAL("returnPressed()"), self.search)
self.connect(self.ui.idEdit, SIGNAL("returnPressed()"), self.searchId)
count = self.config.get('settings', 'maxresultsnumber')
self.ui.countSpin.setValue(int(count))
self.connect(self.ui.actionBackends, SIGNAL("triggered()"), self.backendsConfig)
self.connect(self.ui.actionQuit, SIGNAL("triggered()"), self.close)
self.loadBackendsList()
if self.ui.backendEdit.count() == 0:
self.backendsConfig()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapRecipe, ), self)
if bckndcfg.run():
self.loadBackendsList()
def loadBackendsList(self):
self.ui.backendEdit.clear()
for i, backend in enumerate(self.weboob.iter_backends()):
if i == 0:
self.ui.backendEdit.addItem('All backends', '')
self.ui.backendEdit.addItem(backend.name, backend.name)
if backend.name == self.config.get('settings', 'backend'):
self.ui.backendEdit.setCurrentIndex(i+1)
if self.ui.backendEdit.count() == 0:
self.ui.searchEdit.setEnabled(False)
else:
self.ui.searchEdit.setEnabled(True)
def loadSearchHistory(self):
''' Return search string history list loaded from history file
'''
result = []
history_path = os.path.join(self.weboob.workdir, 'qcookboob_history')
if os.path.exists(history_path):
f = codecs.open(history_path, 'r', 'utf-8')
conf_hist = f.read()
f.close()
if conf_hist is not None and conf_hist.strip() != '':
result = conf_hist.strip().split('\n')
return result
def saveSearchHistory(self):
''' Save search history in history file
'''
if len(self.search_history) > 0:
history_path = os.path.join(self.weboob.workdir, 'qcookboob_history')
f = codecs.open(history_path, 'w', 'utf-8')
f.write('\n'.join(self.search_history))
f.close()
def updateCompletion(self):
qc = QCompleter(QStringList(self.search_history), self)
qc.setCaseSensitivity(Qt.CaseInsensitive)
self.ui.searchEdit.setCompleter(qc)
def getCount(self):
num = self.ui.countSpin.value()
if num == 0:
return None
else:
return num
def stopProcess(self):
self.process.process.finish_event.set()
def doAction(self, description, fun, args):
''' Call fun with args as arguments
and save it in the action history
'''
self.ui.currentActionLabel.setText(description)
if self.action_history['last_action'] is not None:
self.action_history['action_list'].append(self.action_history['last_action'])
self.ui.backButton.setToolTip(self.action_history['last_action']['description'])
self.ui.backButton.show()
self.action_history['last_action'] = {'function': fun, 'args': args, 'description': description}
return fun(*args)
def doBack(self):
''' Go back in action history
Basically call previous function and update history
'''
if len(self.action_history['action_list']) > 0:
todo = self.action_history['action_list'].pop()
self.ui.currentActionLabel.setText(todo['description'])
self.action_history['last_action'] = todo
if len(self.action_history['action_list']) == 0:
self.ui.backButton.hide()
else:
self.ui.backButton.setToolTip(self.action_history['action_list'][-1]['description'])
return todo['function'](*todo['args'])
def search(self):
pattern = unicode(self.ui.searchEdit.text())
# arbitrary max number of completion word
if len(self.search_history) > 50:
self.search_history.pop(0)
if pattern not in self.search_history:
self.search_history.append(pattern)
self.updateCompletion()
self.searchRecipe()
def searchRecipe(self):
pattern = unicode(self.ui.searchEdit.text())
if not pattern:
return
self.doAction(u'Search recipe "%s"' % pattern, self.searchRecipeAction, [pattern])
def searchRecipeAction(self, pattern):
self.ui.stackedWidget.setCurrentWidget(self.ui.list_page)
for mini in self.minis:
self.ui.list_content.layout().removeWidget(mini)
mini.hide()
mini.deleteLater()
self.minis = []
self.ui.searchEdit.setEnabled(False)
QApplication.setOverrideCursor(Qt.WaitCursor)
backend_name = str(self.ui.backendEdit.itemData(self.ui.backendEdit.currentIndex()).toString())
self.process = QtDo(self.weboob, self.addRecipe)
self.process.do(self.app._do_complete, self.getCount(), ('title'), 'iter_recipes', pattern, backends=backend_name, caps=CapRecipe)
self.ui.stopButton.show()
def addRecipe(self, backend, recipe):
if not backend:
self.ui.searchEdit.setEnabled(True)
QApplication.restoreOverrideCursor()
self.process = None
self.ui.stopButton.hide()
return
minirecipe = MiniRecipe(self.weboob, backend, recipe, self)
self.ui.list_content.layout().addWidget(minirecipe)
self.minis.append(minirecipe)
def displayRecipe(self, recipe, backend):
self.ui.stackedWidget.setCurrentWidget(self.ui.info_page)
if self.current_info_widget is not None:
self.ui.info_content.layout().removeWidget(self.current_info_widget)
self.current_info_widget.hide()
self.current_info_widget.deleteLater()
wrecipe = Recipe(recipe, backend, self)
self.ui.info_content.layout().addWidget(wrecipe)
self.current_info_widget = wrecipe
QApplication.restoreOverrideCursor()
def searchId(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
id = unicode(self.ui.idEdit.text())
if '@' in id:
backend_name = id.split('@')[1]
id = id.split('@')[0]
else:
backend_name = None
for backend in self.weboob.iter_backends():
if (backend_name and backend.name == backend_name) or not backend_name:
recipe = backend.get_recipe(id)
if recipe:
self.doAction('Details of recipe "%s"' % recipe.title, self.displayRecipe, [recipe, backend])
QApplication.restoreOverrideCursor()
def closeEvent(self, ev):
self.config.set('settings', 'backend', str(self.ui.backendEdit.itemData(
self.ui.backendEdit.currentIndex()).toString()))
self.saveSearchHistory()
self.config.set('settings', 'maxresultsnumber', self.ui.countSpin.value())
self.config.save()
ev.accept()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/minirecipe.py 0000664 0000000 0000000 00000005107 12411220504 0031006 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import urllib
from PyQt4.QtGui import QFrame, QImage, QPixmap, QApplication
from PyQt4.QtCore import Qt
from weboob.applications.qcookboob.ui.minirecipe_ui import Ui_MiniRecipe
from weboob.capabilities.base import empty
class MiniRecipe(QFrame):
def __init__(self, weboob, backend, recipe, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.ui = Ui_MiniRecipe()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.recipe = recipe
self.ui.titleLabel.setText(recipe.title)
if not empty(recipe.short_description):
if len(recipe.short_description) > 300:
self.ui.shortDescLabel.setText('%s [...]'%recipe.short_description[:300])
else:
self.ui.shortDescLabel.setText(recipe.short_description)
else:
self.ui.shortDescLabel.setText('')
self.ui.backendLabel.setText(backend.name)
self.gotThumbnail()
def gotThumbnail(self):
if not empty(self.recipe.thumbnail_url):
data = urllib.urlopen(self.recipe.thumbnail_url).read()
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToHeight(100,Qt.SmoothTransformation))
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
QApplication.setOverrideCursor(Qt.WaitCursor)
recipe = self.backend.get_recipe(self.recipe.id)
if recipe:
self.parent.doAction('Details of recipe "%s"' %
recipe.title, self.parent.displayRecipe, [recipe, self.backend])
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/qcookboob.py 0000664 0000000 0000000 00000002734 12411220504 0030643 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.recipe import CapRecipe
from weboob.tools.application.qt import QtApplication
from .main_window import MainWindow
class QCookboob(QtApplication):
APPNAME = 'qcookboob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2013 Julien Veyssier'
DESCRIPTION = "Qt application allowing to search recipes."
SHORT_DESCRIPTION = "search recipes"
CAPS = CapRecipe
CONFIG = {'settings': {'backend': '',
'maxresultsnumber': '10'
}
}
def main(self, argv):
self.load_backends([CapRecipe])
self.load_config()
self.main_window = MainWindow(self.config, self.weboob, self)
self.main_window.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/recipe.py 0000664 0000000 0000000 00000010501 12411220504 0030123 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import urllib
import codecs
from PyQt4.QtCore import Qt, SIGNAL
from PyQt4.QtGui import QFrame, QImage, QPixmap, QFileDialog
from weboob.applications.qcookboob.ui.recipe_ui import Ui_Recipe
from weboob.capabilities.base import empty
class Recipe(QFrame):
def __init__(self, recipe, backend, parent=None):
QFrame.__init__(self, parent)
self.parent = parent
self.ui = Ui_Recipe()
self.ui.setupUi(self)
self.connect(self.ui.exportButton, SIGNAL("clicked()"), self.export)
self.recipe = recipe
self.backend = backend
self.gotThumbnail()
self.ui.idEdit.setText(u'%s@%s' % (recipe.id, backend.name))
if not empty(recipe.title):
self.ui.titleLabel.setText(recipe.title)
if not empty(recipe.nb_person):
nbstr = '-'.join(str(num) for num in recipe.nb_person)
self.ui.nbPersonLabel.setText(nbstr)
else:
self.ui.nbPersonLabel.parent().hide()
if not empty(recipe.preparation_time):
self.ui.prepTimeLabel.setText('%s min' % recipe.preparation_time)
else:
self.ui.prepTimeLabel.parent().hide()
if not empty(recipe.cooking_time):
self.ui.cookingTimeLabel.setText('%s min' % recipe.cooking_time)
else:
self.ui.cookingTimeLabel.parent().hide()
if not empty(recipe.ingredients):
txt = u''
for ing in recipe.ingredients:
txt += '* %s\n' % ing
self.ui.ingredientsPlain.setPlainText('%s' % txt)
else:
self.ui.ingredientsPlain.parent().hide()
if not empty(recipe.author):
self.ui.authorLabel.setText('%s' % recipe.author)
else:
self.ui.authorLabel.parent().hide()
if not empty(recipe.instructions):
self.ui.instructionsPlain.setPlainText('%s' % recipe.instructions)
else:
self.ui.instructionsPlain.parent().hide()
if not empty(recipe.comments):
txt = u''
for com in recipe.comments:
txt += '* %s\n' % com
self.ui.commentsPlain.setPlainText('%s' % txt)
else:
self.ui.commentsPlain.parent().hide()
self.ui.verticalLayout.setAlignment(Qt.AlignTop)
self.ui.verticalLayout_2.setAlignment(Qt.AlignTop)
def gotThumbnail(self):
if not empty(self.recipe.picture_url):
data = urllib.urlopen(self.recipe.picture_url).read()
img = QImage.fromData(data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img).scaledToWidth(250, Qt.SmoothTransformation))
def export(self):
fileDial = QFileDialog(self, 'Export "%s" recipe' %
self.recipe.title, '%s.kreml' % self.recipe.title.replace('/', ','), 'Krecipe file (*.kreml);;all files (*)')
fileDial.setAcceptMode(QFileDialog.AcceptSave)
fileDial.setLabelText(QFileDialog.Accept, 'Export recipe')
fileDial.setLabelText(QFileDialog.FileName, 'Recipe file name')
ok = (fileDial.exec_() == 1)
if not ok:
return
result = fileDial.selectedFiles()
if len(result) > 0:
dest = unicode(result[0])
if not dest.endswith('.kreml'):
dest += '.kreml'
data = self.recipe.toKrecipesXml(author=self.backend.name)
try:
with codecs.open(dest, 'w', 'utf-8') as f:
f.write(data)
except IOError as e:
print >>self.stderr, 'Unable to write Krecipe file in "%s": %s' % (dest, e)
return 1
return
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/ui/ 0000775 0000000 0000000 00000000000 12411220504 0026722 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/ui/Makefile 0000664 0000000 0000000 00000000265 12411220504 0030365 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/ui/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0031021 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/ui/main_window.ui0000664 0000000 0000000 00000016707 12411220504 0031607 0 ustar 00root root 0000000 0000000
MainWindow00748463QCookboob4QFrame::StyledPanelQFrame::Raised00Search: 3516777215background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:1, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.479904 rgba(255, 0, 0, 255), stop:0.522685 rgba(255, 255, 255, 255), stop:1 rgba(255, 255, 255, 255));stop<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>max results<html><head/><body><p>Maximum results by backend</p><p>0 = no limit</p></body></html>QFrame::NoFrameQFrame::Raised00Qt::Horizontal40206020<<back75trueQt::AlignCentertrueQt::Horizontal4020true00708261true009626search by ID:0074823toolBarTopToolBarAreafalseBackendsQuitCtrl+Q
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/ui/minirecipe.ui 0000664 0000000 0000000 00000006605 12411220504 0031414 0 ustar 00root root 0000000 0000000
MiniRecipe00464136FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueTitle0050truefalseTextLabeltrue75trueShort description:TextLabeltrue75trueWhereTextLabel
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qcookboob/ui/recipe.ui 0000664 0000000 0000000 00000032533 12411220504 0030536 0 ustar 00root root 0000000 0000000
Recipe006458550080005000FrameQFrame::StyledPanelQFrame::Raised167772153000QFrame::NoFrameQFrame::Raised167772154000QFrame::NoFrameQFrame::RaisedExport to KRecipesML file1677721540QFrame::StyledPanelQFrame::Raised3375trueId:Qt::AlignCentertrue1677721535QFrame::StyledPanelQFrame::Raised75trueTitle:1677721535QFrame::StyledPanelQFrame::Raised75trueAuthor:1677721535QFrame::StyledPanelQFrame::Raised75trueNumber of people:1677721535QFrame::StyledPanelQFrame::Raised75truePreparation time:1677721535QFrame::StyledPanelQFrame::Raised75trueCook time:00167772151000QFrame::StyledPanelQFrame::Raised75trueIngredients:0200Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse00167772151000QFrame::StyledPanelQFrame::Raised1001677721575trueInstructions:0200Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse00167772151000QFrame::StyledPanelQFrame::Raised75trueComments:0100Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/ 0000775 0000000 0000000 00000000000 12411220504 0026300 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/__init__.py 0000664 0000000 0000000 00000000072 12411220504 0030410 0 ustar 00root root 0000000 0000000 from .qflatboob import QFlatBoob
__all__ = ['QFlatBoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/main_window.py 0000664 0000000 0000000 00000036457 12411220504 0031204 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QListWidgetItem, QImage, QPixmap, QLabel, QIcon, QBrush, QColor
from PyQt4.QtCore import SIGNAL, Qt
from decimal import Decimal
from weboob.tools.application.qt import QtMainWindow, QtDo, HTMLDelegate
from weboob.tools.application.qt.backendcfg import BackendCfg
from weboob.capabilities.housing import CapHousing, Query, City
from weboob.capabilities.base import NotLoaded, NotAvailable
from .ui.main_window_ui import Ui_MainWindow
from .query import QueryDialog
class HousingListWidgetItem(QListWidgetItem):
def __init__(self, housing, *args, **kwargs):
QListWidgetItem.__init__(self, *args, **kwargs)
self.housing = housing
self.read = True
def __lt__(self, other):
return '%s%s' % (self.read, Decimal(self.housing.cost or 0) / Decimal(self.housing.area or 1)) < \
'%s%s' % (other.read, Decimal(other.housing.cost or 0) / Decimal(other.housing.area or 1))
def setAttrs(self, storage):
text = u'
%s
' % self.housing.title
text += u'%s — %sm² — %s%s (%s)' % (self.housing.date.strftime('%Y-%m-%d') if self.housing.date else 'Unknown',
self.housing.area, self.housing.cost, self.housing.currency, self.housing.backend)
text += u' %s' % self.housing.text.strip()
text += u' %s' % storage.get('notes', self.housing.fullid, default='').strip().replace('\n', ' ')
self.setText(text)
if not self.housing.fullid in storage.get('read'):
self.setBackground(QBrush(QColor(200, 200, 255)))
self.read = False
elif self.housing.fullid in storage.get('bookmarks'):
self.setBackground(QBrush(QColor(255, 200, 200)))
elif self.background().color() != QColor(0,0,0):
self.setBackground(QBrush())
class MainWindow(QtMainWindow):
def __init__(self, config, storage, weboob, app, parent=None):
QtMainWindow.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.storage = storage
self.weboob = weboob
self.app = app
self.process = None
self.housing = None
self.displayed_photo_idx = 0
self.process_photo = {}
self.process_bookmarks = {}
self.ui.housingsList.setItemDelegate(HTMLDelegate())
self.ui.housingFrame.hide()
self.connect(self.ui.actionBackends, SIGNAL("triggered()"), self.backendsConfig)
self.connect(self.ui.queriesList, SIGNAL('currentIndexChanged(int)'), self.queryChanged)
self.connect(self.ui.addQueryButton, SIGNAL('clicked()'), self.addQuery)
self.connect(self.ui.editQueryButton, SIGNAL('clicked()'), self.editQuery)
self.connect(self.ui.removeQueryButton, SIGNAL('clicked()'), self.removeQuery)
self.connect(self.ui.bookmarksButton, SIGNAL('clicked()'), self.displayBookmarks)
self.connect(self.ui.housingsList, SIGNAL('currentItemChanged(QListWidgetItem*, QListWidgetItem*)'), self.housingSelected)
self.connect(self.ui.previousButton, SIGNAL('clicked()'), self.previousClicked)
self.connect(self.ui.nextButton, SIGNAL('clicked()'), self.nextClicked)
self.connect(self.ui.bookmark, SIGNAL('stateChanged(int)'), self.bookmarkChanged)
self.reloadQueriesList()
self.refreshHousingsList()
if self.weboob.count_backends() == 0:
self.backendsConfig()
if len(self.config.get('queries')) == 0:
self.addQuery()
def closeEvent(self, event):
self.setHousing(None)
QtMainWindow.closeEvent(self, event)
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapHousing,), self)
if bckndcfg.run():
pass
def reloadQueriesList(self, select_name=None):
self.disconnect(self.ui.queriesList, SIGNAL('currentIndexChanged(int)'), self.queryChanged)
self.ui.queriesList.clear()
for name in self.config.get('queries', default={}).iterkeys():
self.ui.queriesList.addItem(name)
if name == select_name:
self.ui.queriesList.setCurrentIndex(len(self.ui.queriesList)-1)
self.connect(self.ui.queriesList, SIGNAL('currentIndexChanged(int)'), self.queryChanged)
if select_name is not None:
self.queryChanged()
def removeQuery(self):
name = unicode(self.ui.queriesList.itemText(self.ui.queriesList.currentIndex()))
queries = self.config.get('queries')
queries.pop(name, None)
self.config.set('queries', queries)
self.config.save()
self.reloadQueriesList()
self.queryChanged()
def editQuery(self):
name = unicode(self.ui.queriesList.itemText(self.ui.queriesList.currentIndex()))
self.addQuery(name)
def addQuery(self, name=None):
querydlg = QueryDialog(self.weboob, self)
if name is not None:
query = self.config.get('queries', name)
querydlg.ui.nameEdit.setText(name)
querydlg.ui.nameEdit.setEnabled(False)
for c in query['cities']:
city = City(c['id'])
city.backend = c['backend']
city.name = c['name']
item = querydlg.buildCityItem(city)
querydlg.ui.citiesList.addItem(item)
querydlg.ui.typeBox.setCurrentIndex(int(query.get('type', 0)))
querydlg.ui.areaMin.setValue(query['area_min'])
querydlg.ui.areaMax.setValue(query['area_max'])
querydlg.ui.costMin.setValue(query['cost_min'])
querydlg.ui.costMax.setValue(query['cost_max'])
querydlg.selectComboValue(querydlg.ui.nbRooms, query['nb_rooms'])
if querydlg.exec_():
name = unicode(querydlg.ui.nameEdit.text())
query = {}
query['type'] = querydlg.ui.typeBox.currentIndex()
query['cities'] = []
for i in xrange(len(querydlg.ui.citiesList)):
item = querydlg.ui.citiesList.item(i)
city = item.data(Qt.UserRole).toPyObject()
query['cities'].append({'id': city.id, 'backend': city.backend, 'name': city.name})
query['area_min'] = querydlg.ui.areaMin.value()
query['area_max'] = querydlg.ui.areaMax.value()
query['cost_min'] = querydlg.ui.costMin.value()
query['cost_max'] = querydlg.ui.costMax.value()
try:
query['nb_rooms'] = int(querydlg.ui.nbRooms.itemText(querydlg.ui.nbRooms.currentIndex()))
except ValueError:
query['nb_rooms'] = 0
self.config.set('queries', name, query)
self.config.save()
self.reloadQueriesList(name)
def queryChanged(self, i=None):
self.refreshHousingsList()
def refreshHousingsList(self):
name = unicode(self.ui.queriesList.itemText(self.ui.queriesList.currentIndex()))
q = self.config.get('queries', name)
if q is None:
return q
self.ui.housingsList.clear()
self.ui.queriesList.setEnabled(False)
self.ui.bookmarksButton.setEnabled(False)
query = Query()
query.type = int(q.get('type', 0))
query.cities = []
for c in q['cities']:
city = City(c['id'])
city.backend = c['backend']
city.name = c['name']
query.cities.append(city)
query.area_min = int(q['area_min']) or None
query.area_max = int(q['area_max']) or None
query.cost_min = int(q['cost_min']) or None
query.cost_max = int(q['cost_max']) or None
query.nb_rooms = int(q['nb_rooms']) or None
self.process = QtDo(self.weboob, self.addHousing)
self.process.do(self.app._do_complete, 20, (), 'search_housings', query)
def displayBookmarks(self):
self.ui.housingsList.clear()
self.ui.queriesList.setEnabled(False)
self.ui.queriesList.setCurrentIndex(-1)
self.ui.bookmarksButton.setEnabled(False)
self.processes = {}
for id in self.storage.get('bookmarks'):
_id, backend_name = id.rsplit('@', 1)
self.process_bookmarks[id] = QtDo(self.weboob, self.addHousing)
self.process_bookmarks[id].do('get_housing', _id, backends=backend_name)
def addHousing(self, backend, housing):
if not backend:
self.ui.queriesList.setEnabled(True)
self.ui.bookmarksButton.setEnabled(True)
self.process = None
return
if not housing:
return
item = HousingListWidgetItem(housing)
item.setAttrs(self.storage)
if housing.photos is NotLoaded:
process = QtDo(self.weboob, lambda b, c: self.setPhoto(c, item))
process.do('fillobj', housing, ['photos'], backends=housing.backend)
self.process_photo[housing.id] = process
elif housing.photos is not NotAvailable and len(housing.photos) > 0:
if not self.setPhoto(housing, item):
photo = housing.photos[0]
process = QtDo(self.weboob, lambda b, p: self.setPhoto(housing, item))
process.do('fillobj', photo, ['data'], backends=housing.backend)
self.process_photo[housing.id] = process
self.ui.housingsList.addItem(item)
if housing.fullid in self.process_bookmarks:
self.process_bookmarks.pop(housing.fullid)
def housingSelected(self, item, prev):
if item is not None:
housing = item.housing
self.ui.queriesFrame.setEnabled(False)
read = set(self.storage.get('read'))
read.add(housing.fullid)
self.storage.set('read', list(read))
self.storage.save()
self.process = QtDo(self.weboob, self.gotHousing)
self.process.do('fillobj', housing, backends=housing.backend)
else:
housing = None
self.setHousing(housing)
if prev:
prev.setAttrs(self.storage)
def setPhoto(self, housing, item):
if not housing:
return False
try:
self.process_photo.pop(housing.id, None)
except KeyError:
pass
if not housing.photos:
return False
img = None
for photo in housing.photos:
if photo.data:
img = QImage.fromData(photo.data)
break
if img:
item.setIcon(QIcon(QPixmap.fromImage(img)))
return True
return False
def setHousing(self, housing, nottext='Loading...'):
if self.housing is not None:
self.saveNotes()
self.housing = housing
if self.housing is None:
self.ui.housingFrame.hide()
return
self.ui.housingFrame.show()
self.display_photo()
self.ui.bookmark.setChecked(housing.fullid in self.storage.get('bookmarks'))
self.ui.titleLabel.setText('
%s
' % housing.title)
self.ui.areaLabel.setText(u'%s m²' % housing.area)
self.ui.costLabel.setText(u'%s %s' % (housing.cost, housing.currency))
self.ui.dateLabel.setText(housing.date.strftime('%Y-%m-%d') if housing.date else nottext)
self.ui.phoneLabel.setText(housing.phone or nottext)
self.ui.locationLabel.setText(housing.location or nottext)
self.ui.stationLabel.setText(housing.station or nottext)
if housing.text:
self.ui.descriptionEdit.setText(housing.text.replace('\n', ' '))
else:
self.ui.descriptionEdit.setText(nottext)
self.ui.notesEdit.setText(self.storage.get('notes', housing.fullid, default=''))
while self.ui.detailsFrame.layout().count() > 0:
child = self.ui.detailsFrame.layout().takeAt(0)
child.widget().hide()
child.widget().deleteLater()
if housing.details:
for key, value in housing.details.iteritems():
label = QLabel(value)
label.setTextInteractionFlags(Qt.TextSelectableByMouse|Qt.LinksAccessibleByMouse)
self.ui.detailsFrame.layout().addRow('%s:' % key, label)
def gotHousing(self, backend, housing):
if not backend:
self.ui.queriesFrame.setEnabled(True)
self.process = None
return
self.setHousing(housing, nottext='')
def bookmarkChanged(self, state):
bookmarks = set(self.storage.get('bookmarks'))
if state == Qt.Checked:
bookmarks.add(self.housing.fullid)
elif self.housing.fullid in bookmarks:
bookmarks.remove(self.housing.fullid)
self.storage.set('bookmarks', list(bookmarks))
self.storage.save()
def saveNotes(self):
if not self.housing:
return
txt = unicode(self.ui.notesEdit.toPlainText()).strip()
if len(txt) > 0:
self.storage.set('notes', self.housing.fullid, txt)
else:
self.storage.delete('notes', self.housing.fullid)
self.storage.save()
def previousClicked(self):
if not self.housing.photos or len(self.housing.photos) == 0:
return
self.displayed_photo_idx = (self.displayed_photo_idx - 1) % len(self.housing.photos)
self.display_photo()
def nextClicked(self):
if not self.housing.photos or len(self.housing.photos) == 0:
return
self.displayed_photo_idx = (self.displayed_photo_idx + 1) % len(self.housing.photos)
self.display_photo()
def display_photo(self):
if not self.housing.photos:
self.ui.photosFrame.hide()
return
if self.displayed_photo_idx >= len(self.housing.photos):
self.displayed_photo_idx = len(self.housing.photos) - 1
if self.displayed_photo_idx < 0:
self.ui.photosFrame.hide()
return
self.ui.photosFrame.show()
photo = self.housing.photos[self.displayed_photo_idx]
if photo.data:
data = photo.data
if photo.id in self.process_photo:
self.process_photo.pop(photo.id)
else:
self.process_photo[photo.id] = QtDo(self.weboob, lambda b,p: self.display_photo())
self.process_photo[photo.id].do('fillobj', photo, ['data'], backends=self.housing.backend)
return
img = QImage.fromData(data)
img = img.scaledToWidth(self.width()/3)
self.ui.photoLabel.setPixmap(QPixmap.fromImage(img))
if photo.url is not NotLoaded:
text = '%s' % (photo.url, photo.url)
self.ui.photoUrlLabel.setText(text)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/qflatboob.py 0000664 0000000 0000000 00000003040 12411220504 0030620 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.housing import CapHousing
from weboob.tools.application.qt import QtApplication
from weboob.tools.config.yamlconfig import YamlConfig
from .main_window import MainWindow
class QFlatBoob(QtApplication):
APPNAME = 'qflatboob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2012 Romain Bignon'
DESCRIPTION = "Qt application to search for housing."
SHORT_DESCRIPTION = "search for housing"
CAPS = CapHousing
CONFIG = {'queries': {}}
STORAGE = {'bookmarks': [], 'read': [], 'notes': {}}
def main(self, argv):
self.load_backends(CapHousing)
self.create_storage()
self.load_config(klass=YamlConfig)
self.main_window = MainWindow(self.config, self.storage, self.weboob, self)
self.main_window.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/query.py 0000664 0000000 0000000 00000007077 12411220504 0030032 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QDialog, QListWidgetItem, QMessageBox
from PyQt4.QtCore import SIGNAL, Qt
from weboob.tools.application.qt import QtDo, HTMLDelegate
from .ui.query_ui import Ui_QueryDialog
class QueryDialog(QDialog):
def __init__(self, weboob, parent=None):
QDialog.__init__(self, parent)
self.ui = Ui_QueryDialog()
self.ui.setupUi(self)
self.weboob = weboob
self.ui.resultsList.setItemDelegate(HTMLDelegate())
self.ui.citiesList.setItemDelegate(HTMLDelegate())
self.search_process = None
self.connect(self.ui.cityEdit, SIGNAL('returnPressed()'), self.searchCity)
self.connect(self.ui.resultsList, SIGNAL('itemDoubleClicked(QListWidgetItem*)'), self.insertCity)
self.connect(self.ui.citiesList, SIGNAL('itemDoubleClicked(QListWidgetItem*)'), self.removeCity)
self.connect(self.ui.buttonBox, SIGNAL('accepted()'), self.okButton)
if hasattr(self.ui.cityEdit, "setPlaceholderText"):
self.ui.cityEdit.setPlaceholderText("Press enter to search city")
def keyPressEvent(self, event):
"""
Disable handler and to prevent closing the window.
"""
event.ignore()
def selectComboValue(self, box, value):
for i in xrange(box.count()):
if box.itemText(i) == str(value):
box.setCurrentIndex(i)
break
def searchCity(self):
pattern = unicode(self.ui.cityEdit.text())
self.ui.resultsList.clear()
self.ui.cityEdit.clear()
self.ui.cityEdit.setEnabled(False)
self.search_process = QtDo(self.weboob, self.addResult)
self.search_process.do('search_city', pattern)
def addResult(self, backend, city):
if not backend or not city:
self.search_process = None
self.ui.cityEdit.setEnabled(True)
return
item = self.buildCityItem(city)
self.ui.resultsList.addItem(item)
self.ui.resultsList.sortItems()
def buildCityItem(self, city):
item = QListWidgetItem()
item.setText('%s (%s)' % (city.name, city.backend))
item.setData(Qt.UserRole, city)
return item
def insertCity(self, i):
item = QListWidgetItem()
item.setText(i.text())
item.setData(Qt.UserRole, i.data(Qt.UserRole))
self.ui.citiesList.addItem(item)
def removeCity(self, item):
self.ui.citiesList.removeItemWidget(item)
def okButton(self):
if not self.ui.nameEdit.text():
QMessageBox.critical(self, self.tr('Error'), self.tr('Please enter a name to your query.'), QMessageBox.Ok)
return
if self.ui.citiesList.count() == 0:
QMessageBox.critical(self, self.tr('Error'), self.tr('Please add at least one city.'), QMessageBox.Ok)
return
self.accept()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/ui/ 0000775 0000000 0000000 00000000000 12411220504 0026715 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/ui/Makefile 0000664 0000000 0000000 00000000265 12411220504 0030360 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/ui/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0031014 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/ui/main_window.ui0000664 0000000 0000000 00000044471 12411220504 0031601 0 ustar 00root root 0000000 0000000
MainWindow00709572QFlatBoobQt::Horizontal00QFrame::StyledPanelQFrame::Raised+edit-00QAbstractItemView::NoEditTriggers128128trueBookmarksQt::Horizontal10QFrame::StyledPanelQFrame::RaisedQt::Vertical00200<h1>Loading...</h1>QFormLayout::ExpandingFieldsGrow75trueCostLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse75trueArea75trueDate75truePhone75trueLocation75trueStationLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::VerticalQSizePolicy::Expanding205QFrame::StyledPanelQFrame::Raised002016777215<Qt::AlignCenter002016777215>Qt::AlignCenterQt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::VerticalQSizePolicy::Expanding135Qt::HorizontalQt::Vertical050true00falseQFrame::NoFrameQFrame::PlainQFormLayout::ExpandingFieldsGrow200070924toolBarTopToolBarAreafalseBackendsaddQueryButtonqueriesListeditQueryButtonremoveQueryButtonhousingsListbookmarksButtonbookmarkpreviousButtonnextButtondescriptionEdit
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qflatboob/ui/query.ui 0000664 0000000 0000000 00000021440 12411220504 0030422 0 ustar 00root root 0000000 0000000
QueryDialog00551457Add a queryQFrame::StyledPanelQFrame::RaisedName of this query:RentSaleCitiesNumber of roomsN/A12345AreaMinMax99995m²99995m²CostMinMax99999999100€99999999100€Qt::HorizontalQDialogButtonBox::Cancel|QDialogButtonBox::OknameEditcityEditresultsListcitiesListnbRoomsareaMinareaMaxcostMincostMaxbuttonBoxbuttonBoxrejected()QueryDialogreject()316260286274
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhandjoob/ 0000775 0000000 0000000 00000000000 12411220504 0026274 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhandjoob/__init__.py 0000664 0000000 0000000 00000000072 12411220504 0030404 0 ustar 00root root 0000000 0000000 from .qhandjoob import QHandJoob
__all__ = ['QHandJoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhandjoob/main_window.py 0000664 0000000 0000000 00000017673 12411220504 0031177 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Sébastien Monel
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QListWidgetItem, QApplication, QCompleter
from PyQt4.QtCore import SIGNAL, Qt, QStringList
from weboob.tools.application.qt import QtMainWindow, QtDo
from weboob.tools.application.qt.backendcfg import BackendCfg
from weboob.capabilities.job import CapJob
from .ui.main_window_ui import Ui_MainWindow
import os
import codecs
class JobListWidgetItem(QListWidgetItem):
def __init__(self, job, *args, **kwargs):
QListWidgetItem.__init__(self, *args, **kwargs)
self.job = job
def __lt__(self, other):
if self.job.publication_date and other.job.publication_date:
return self.job.publication_date < other.job.publication_date
else:
return False
def setAttrs(self, storage):
text = u'%s - %s' % (self.job.backend, self.job.title)
self.setText(text)
class MainWindow(QtMainWindow):
def __init__(self, config, storage, weboob, parent=None):
QtMainWindow.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.storage = storage
self.weboob = weboob
self.process = None
self.displayed_photo_idx = 0
self.process_photo = {}
self.process_bookmarks = {}
# search history is a list of patterns which have been searched
self.search_history = self.loadSearchHistory()
self.updateCompletion()
self.ui.jobFrame.hide()
self.connect(self.ui.actionBackends, SIGNAL("triggered()"), self.backendsConfig)
self.connect(self.ui.searchEdit, SIGNAL('returnPressed()'), self.doSearch)
self.connect(self.ui.jobList, SIGNAL('currentItemChanged(QListWidgetItem*, QListWidgetItem*)'), self.jobSelected)
self.connect(self.ui.searchButton, SIGNAL('clicked()'), self.doSearch)
self.connect(self.ui.refreshButton, SIGNAL('clicked()'), self.doAdvancedSearch)
self.connect(self.ui.queriesTabWidget, SIGNAL('currentChanged(int)'), self.tabChange)
self.connect(self.ui.jobListAdvancedSearch, SIGNAL('currentItemChanged(QListWidgetItem*, QListWidgetItem*)'), self.jobSelected)
self.connect(self.ui.idEdit, SIGNAL('returnPressed()'), self.openJob)
if self.weboob.count_backends() == 0:
self.backendsConfig()
def loadSearchHistory(self):
''' Return search string history list loaded from history file
'''
result = []
history_path = os.path.join(self.weboob.workdir, 'qhandjoob_history')
if os.path.exists(history_path):
f = codecs.open(history_path, 'r', 'utf-8')
conf_hist = f.read()
f.close()
if conf_hist is not None and conf_hist.strip() != '':
result = conf_hist.strip().split('\n')
return result
def saveSearchHistory(self):
''' Save search history in history file
'''
if len(self.search_history) > 0:
history_path = os.path.join(self.weboob.workdir, 'qhandjoob_history')
f = codecs.open(history_path, 'w', 'utf-8')
f.write('\n'.join(self.search_history))
f.close()
def updateCompletion(self):
qc = QCompleter(QStringList(self.search_history), self)
qc.setCaseSensitivity(Qt.CaseInsensitive)
self.ui.searchEdit.setCompleter(qc)
def tabChange(self, index):
if index == 1:
self.doAdvancedSearch()
def doAdvancedSearch(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
self.ui.jobListAdvancedSearch.clear()
self.process = QtDo(self.weboob, self.addJobAdvancedSearch)
self.process.do('advanced_search_job')
def doSearch(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
pattern = unicode(self.ui.searchEdit.text())
# arbitrary max number of completion word
if pattern:
if len(self.search_history) > 50:
self.search_history.pop(0)
if pattern not in self.search_history:
self.search_history.append(pattern)
self.updateCompletion()
self.ui.jobList.clear()
self.process = QtDo(self.weboob, self.addJobSearch)
self.process.do('search_job', pattern)
def addJobSearch(self, backend, job):
item = self.addJob(backend, job)
if item:
self.ui.jobList.addItem(item)
if not backend:
QApplication.restoreOverrideCursor()
def addJobAdvancedSearch(self, backend, job):
item = self.addJob(backend, job)
if item:
self.ui.jobListAdvancedSearch.addItem(item)
if not backend:
QApplication.restoreOverrideCursor()
def addJob(self, backend, job):
if not backend:
self.process = None
return
if not job:
return
item = JobListWidgetItem(job)
item.setAttrs(self.storage)
return item
def closeEvent(self, event):
self.saveSearchHistory()
QtMainWindow.closeEvent(self, event)
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapJob,), self)
if bckndcfg.run():
pass
def jobSelected(self, item, prev):
QApplication.setOverrideCursor(Qt.WaitCursor)
if item is not None:
job = item.job
self.ui.queriesTabWidget.setEnabled(False)
self.process = QtDo(self.weboob, self.gotJob)
self.process.do('fillobj', job, backends=job.backend)
else:
job = None
self.setJob(job)
if prev:
prev.setAttrs(self.storage)
def openJob(self):
QApplication.setOverrideCursor(Qt.WaitCursor)
url = unicode(self.ui.idEdit.text())
if not url:
return
for backend in self.weboob.iter_backends():
job = backend.get_job_advert(url)
if job:
self.process = QtDo(self.weboob, self.gotJob)
self.process.do('fillobj', job, backends=job.backend)
break
self.setJob(job)
self.ui.idEdit.clear()
QApplication.restoreOverrideCursor()
def gotJob(self, backend, job):
if not backend:
self.ui.queriesTabWidget.setEnabled(True)
self.process = None
return
self.setJob(job)
def setJob(self, job):
if job:
self.ui.descriptionEdit.setText("%s" % job.description)
self.ui.titleLabel.setText("
%s
" % job.title)
self.ui.idLabel.setText("%s" % job.id)
self.ui.jobNameLabel.setText("%s" % job.job_name)
self.ui.publicationDateLabel.setText("%s" % job.publication_date)
self.ui.societyNameLabel.setText("%s" % job.society_name)
self.ui.placeLabel.setText("%s" % job.place)
self.ui.payLabel.setText("%s" % job.pay)
self.ui.contractTypeLabel.setText("%s" % job.contract_type)
self.ui.formationLabel.setText("%s" % job.formation)
self.ui.experienceLabel.setText("%s" % job.experience)
self.ui.urlLabel.setText("%s" % (job.url, job.url))
self.ui.jobFrame.show()
else:
self.ui.jobFrame.hide()
QApplication.restoreOverrideCursor()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhandjoob/qhandjoob.py 0000664 0000000 0000000 00000002777 12411220504 0030630 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Sébastien Monel
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.job import CapJob
from weboob.tools.application.qt import QtApplication
from weboob.tools.config.yamlconfig import YamlConfig
from .main_window import MainWindow
class QHandJoob(QtApplication):
APPNAME = 'qhandjoob'
VERSION = '1.0'
COPYRIGHT = u'Copyright(C) 2013 Sébastien Monel'
DESCRIPTION = "Qt application to search for job."
SHORT_DESCRIPTION = "search for job"
CAPS = CapJob
CONFIG = {'queries': {}}
STORAGE = {'bookmarks': [], 'read': [], 'notes': {}}
def main(self, argv):
self.load_backends(CapJob)
self.create_storage()
self.load_config(klass=YamlConfig)
self.main_window = MainWindow(self.config, self.storage, self.weboob)
self.main_window.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhandjoob/ui/ 0000775 0000000 0000000 00000000000 12411220504 0026711 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhandjoob/ui/Makefile 0000664 0000000 0000000 00000000265 12411220504 0030354 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhandjoob/ui/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0031010 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhandjoob/ui/main_window.ui0000664 0000000 0000000 00000041503 12411220504 0031566 0 ustar 00root root 0000000 0000000
MainWindow00709572QHandJoobQt::Horizontal001Searchsearch00QAbstractItemView::NoEditTriggers128128true id : Advanced search00QAbstractItemView::NoEditTriggers128128trueRefreshQt::Horizontal10QFrame::StyledPanelQFrame::RaisedQt::Vertical<h1>Loading...</h1>QFormLayout::ExpandingFieldsGrow75trueIdLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse75trueJob Name75truePublication Date75trueSociety Name75truePlace75truePay75trueContract Type75trueFormation75trueExperience75trueURLLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouseLoading...truetrueQt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::VerticalQSizePolicy::Expanding205050true0070923toolBarTopToolBarAreafalseBackendssearchButtonjobList
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ 0000775 0000000 0000000 00000000000 12411220504 0026271 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/__init__.py 0000664 0000000 0000000 00000000072 12411220504 0030401 0 ustar 00root root 0000000 0000000 from .qhavedate import QHaveDate
__all__ = ['QHaveDate']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/contacts.py 0000664 0000000 0000000 00000054151 12411220504 0030467 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import time
import logging
from PyQt4.QtGui import QWidget, QListWidgetItem, QImage, QIcon, QPixmap, \
QFrame, QMessageBox, QTabWidget, QVBoxLayout, \
QFormLayout, QLabel, QPushButton
from PyQt4.QtCore import SIGNAL, Qt
from weboob.tools.application.qt import QtDo, HTMLDelegate
from weboob.tools.misc import to_unicode
from weboob.capabilities.contact import CapContact, Contact
from weboob.capabilities.chat import CapChat
from weboob.capabilities.messages import CapMessages, CapMessagesPost, Message
from weboob.capabilities.base import NotLoaded
from .ui.contacts_ui import Ui_Contacts
from .ui.contact_thread_ui import Ui_ContactThread
from .ui.thread_message_ui import Ui_ThreadMessage
from .ui.profile_ui import Ui_Profile
from .ui.notes_ui import Ui_Notes
class ThreadMessage(QFrame):
"""
This class represents a message in the thread tab.
"""
def __init__(self, message, parent=None):
QFrame.__init__(self, parent)
self.ui = Ui_ThreadMessage()
self.ui.setupUi(self)
self.set_message(message)
def set_message(self, message):
self.message = message
self.ui.nameLabel.setText(message.sender)
header = time.strftime('%Y-%m-%d %H:%M:%S', message.date.timetuple())
if message.flags & message.IS_NOT_RECEIVED:
header += u' — Unread'
elif message.flags & message.IS_RECEIVED:
header += u' — Read'
self.ui.headerLabel.setText(header)
if message.flags & message.IS_HTML:
content = message.content
else:
content = message.content.replace('&', '&').replace('<', '<').replace('>', '>').replace('\n', ' ')
self.ui.contentLabel.setText(content)
def __eq__(self, m):
if not isinstance(m, Message):
return False
return self.message == m.message
class ContactThread(QWidget):
"""
The thread of the selected contact.
"""
def __init__(self, weboob, contact, support_reply, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_ContactThread()
self.ui.setupUi(self)
self.weboob = weboob
self.contact = contact
self.thread = None
self.messages = []
self.process_msg = None
self.connect(self.ui.refreshButton, SIGNAL('clicked()'), self.refreshMessages)
if support_reply:
self.connect(self.ui.sendButton, SIGNAL('clicked()'), self.postReply)
else:
self.ui.frame.hide()
self.refreshMessages()
def refreshMessages(self, fillobj=False):
if self.process_msg:
return
self.ui.refreshButton.setEnabled(False)
self.process_msg = QtDo(self.weboob, self.gotThread, self.gotError)
if fillobj and self.thread:
self.process_msg.do('fillobj', self.thread, ['root'], backends=self.contact.backend)
else:
self.process_msg.do('get_thread', self.contact.id, backends=self.contact.backend)
def gotError(self, backend, error, backtrace):
self.ui.textEdit.setEnabled(False)
self.ui.sendButton.setEnabled(False)
self.ui.refreshButton.setEnabled(True)
def gotThread(self, backend, thread):
if not thread:
#v = self.ui.scrollArea.verticalScrollBar()
#print v.minimum(), v.value(), v.maximum(), v.sliderPosition()
#self.ui.scrollArea.verticalScrollBar().setValue(self.ui.scrollArea.verticalScrollBar().maximum())
self.process_msg = None
return
self.ui.textEdit.setEnabled(True)
self.ui.sendButton.setEnabled(True)
self.ui.refreshButton.setEnabled(True)
self.thread = thread
if thread.root is NotLoaded:
self._insert_load_button(0)
else:
for message in thread.iter_all_messages():
self._insert_message(message)
def _insert_message(self, message):
widget = ThreadMessage(message)
if widget in self.messages:
old_widget = self.messages[self.messages.index(widget)]
if old_widget.message.flags != widget.message.flags:
old_widget.set_message(widget.message)
return
for i, m in enumerate(self.messages):
if widget.message.date > m.message.date:
self.ui.scrollAreaContent.layout().insertWidget(i, widget)
self.messages.insert(i, widget)
if message.parent is NotLoaded:
self._insert_load_button(i)
return
self.ui.scrollAreaContent.layout().addWidget(widget)
self.messages.append(widget)
if message.parent is NotLoaded:
self._insert_load_button(-1)
def _insert_load_button(self, pos):
button = QPushButton(self.tr('More messages...'))
self.connect(button, SIGNAL('clicked()'), lambda: self._load_button_pressed(button))
if pos >= 0:
self.ui.scrollAreaContent.layout().insertWidget(pos, button)
else:
self.ui.scrollAreaContent.layout().addWidget(button)
def _load_button_pressed(self, button):
self.ui.scrollAreaContent.layout().removeWidget(button)
button.hide()
button.deleteLater()
self.refreshMessages(fillobj=True)
def postReply(self):
text = unicode(self.ui.textEdit.toPlainText())
self.ui.textEdit.setEnabled(False)
self.ui.sendButton.setEnabled(False)
m = Message(thread=self.thread,
id=0,
title=u'',
sender=None,
receivers=None,
content=text,
parent=self.messages[0].message if len(self.messages) > 0 else None)
self.process_reply = QtDo(self.weboob, self._postReply_cb, self._postReply_eb)
self.process_reply.do('post_message', m, backends=self.contact.backend)
def _postReply_cb(self, backend, ignored):
if not backend:
return
self.ui.textEdit.clear()
self.ui.textEdit.setEnabled(True)
self.ui.sendButton.setEnabled(True)
self.refreshMessages()
self.process_reply = None
def _postReply_eb(self, backend, error, backtrace):
content = unicode(self.tr('Unable to send message:\n%s\n')) % to_unicode(error)
if logging.root.level == logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while posting reply'),
content, QMessageBox.Ok)
self.process_reply = None
class ContactProfile(QWidget):
def __init__(self, weboob, contact, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_Profile()
self.ui.setupUi(self)
self.connect(self.ui.previousButton, SIGNAL('clicked()'), self.previousClicked)
self.connect(self.ui.nextButton, SIGNAL('clicked()'), self.nextClicked)
self.weboob = weboob
self.contact = contact
self.loaded_profile = False
self.displayed_photo_idx = 0
self.process_photo = {}
missing_fields = self.gotProfile(self.weboob.get_backend(contact.backend), contact)
if len(missing_fields) > 0:
self.process_contact = QtDo(self.weboob, self.gotProfile, self.gotError)
self.process_contact.do('fillobj', self.contact, missing_fields, backends=self.contact.backend)
def gotError(self, backend, error, backtrace):
self.ui.frame_photo.hide()
self.ui.descriptionEdit.setText('
Unable to show profile
%s
' % to_unicode(error))
def gotProfile(self, backend, contact):
if not backend:
return []
missing_fields = set()
self.display_photo()
self.ui.nicknameLabel.setText('
' % contact.summary.replace('\n', ' '))
if not contact.profile:
missing_fields.add('profile')
elif not self.loaded_profile:
self.loaded_profile = True
for head in contact.profile.itervalues():
if head.flags & head.HEAD:
widget = self.ui.headWidget
else:
widget = self.ui.profileTab
self.process_node(head, widget)
return missing_fields
def process_node(self, node, widget):
# Set the value widget
value = None
if node.flags & node.SECTION:
value = QWidget()
value.setLayout(QFormLayout())
for sub in node.value.itervalues():
self.process_node(sub, value)
elif isinstance(node.value, list):
value = QLabel(' '.join(unicode(s) for s in node.value))
value.setWordWrap(True)
elif isinstance(node.value, tuple):
value = QLabel(', '.join(unicode(s) for s in node.value))
value.setWordWrap(True)
elif isinstance(node.value, (basestring,int,long,float)):
value = QLabel(unicode(node.value))
else:
logging.warning('Not supported value: %r' % node.value)
return
if isinstance(value, QLabel):
value.setTextInteractionFlags(Qt.TextSelectableByMouse|Qt.TextSelectableByKeyboard|Qt.LinksAccessibleByMouse)
# Insert the value widget into the parent widget, depending
# of its type.
if isinstance(widget, QTabWidget):
widget.addTab(value, node.label)
elif isinstance(widget.layout(), QFormLayout):
label = QLabel(u'%s: ' % node.label)
widget.layout().addRow(label, value)
elif isinstance(widget.layout(), QVBoxLayout):
widget.layout().addWidget(QLabel(u'
%s
' % node.label))
widget.layout().addWidget(value)
else:
logging.warning('Not supported widget: %r' % widget)
def previousClicked(self):
if len(self.contact.photos) == 0:
return
self.displayed_photo_idx = (self.displayed_photo_idx - 1) % len(self.contact.photos)
self.display_photo()
def nextClicked(self):
if len(self.contact.photos) == 0:
return
self.displayed_photo_idx = (self.displayed_photo_idx + 1) % len(self.contact.photos)
self.display_photo()
def display_photo(self):
if self.displayed_photo_idx >= len(self.contact.photos) or self.displayed_photo_idx < 0:
self.displayed_photo_idx = len(self.contact.photos) - 1
if self.displayed_photo_idx < 0:
self.ui.photoUrlLabel.setText('')
return
photo = self.contact.photos.values()[self.displayed_photo_idx]
if photo.data:
data = photo.data
if photo.id in self.process_photo:
self.process_photo.pop(photo.id)
else:
self.process_photo[photo.id] = QtDo(self.weboob, lambda b,p: self.display_photo())
self.process_photo[photo.id].do('fillobj', photo, ['data'], backends=self.contact.backend)
if photo.thumbnail_data:
data = photo.thumbnail_data
else:
return
img = QImage.fromData(data)
img = img.scaledToWidth(self.width()/3)
self.ui.photoLabel.setPixmap(QPixmap.fromImage(img))
if photo.url is not NotLoaded:
text = '%s' % (photo.url, photo.url)
if photo.hidden:
text += ' (Hidden photo)'
self.ui.photoUrlLabel.setText(text)
class ContactNotes(QWidget):
""" Widget for storing notes about a contact """
def __init__(self, weboob, contact, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_Notes()
self.ui.setupUi(self)
self.weboob = weboob
self.contact = contact
self.ui.textEdit.setEnabled(False)
self.ui.saveButton.setEnabled(False)
self.process = QtDo(self.weboob, self._getNotes_cb, self._getNotes_eb)
self.process.do('get_notes', self.contact.id, backends=(self.contact.backend,))
self.connect(self.ui.saveButton, SIGNAL('clicked()'), self.saveNotes)
def _getNotes_cb(self, backend, data):
if not backend or not data:
self.process = None
self.ui.textEdit.setEnabled(True)
self.ui.saveButton.setEnabled(True)
return
self.ui.textEdit.setText(data)
def _getNotes_eb(self, backend, error, backtrace):
if isinstance(error, NotImplementedError):
return
self.ui.textEdit.setEnabled(True)
self.ui.saveButton.setEnabled(True)
content = unicode(self.tr('Unable to load notes:\n%s\n')) % to_unicode(error)
if logging.root.level == logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while loading notes'),
content, QMessageBox.Ok)
def saveNotes(self):
text = unicode(self.ui.textEdit.toPlainText())
self.ui.saveButton.setEnabled(False)
self.ui.textEdit.setEnabled(False)
self.process = QtDo(self.weboob, self._saveNotes_cb, self._saveNotes_eb)
self.process.do('save_notes', self.contact.id, text, backends=(self.contact.backend,))
def _saveNotes_cb(self, backend, data):
self.ui.saveButton.setEnabled(True)
self.ui.textEdit.setEnabled(True)
pass
def _saveNotes_eb(self, backend, error, backtrace):
self.ui.saveButton.setEnabled(True)
self.ui.textEdit.setEnabled(True)
content = unicode(self.tr('Unable to save notes:\n%s\n')) % to_unicode(error)
if logging.root.level == logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while saving notes'),
content, QMessageBox.Ok)
class IGroup(object):
def __init__(self, weboob, id, name):
self.id = id
self.name = name
self.weboob = weboob
def iter_contacts(self, cb):
raise NotImplementedError()
class MetaGroup(IGroup):
def iter_contacts(self, cb):
if self.id == 'online':
status = Contact.STATUS_ONLINE|Contact.STATUS_AWAY
elif self.id == 'offline':
status = Contact.STATUS_OFFLINE
else:
status = Contact.STATUS_ALL
self.process = QtDo(self.weboob, lambda b, d: self.cb(cb, b, d))
self.process.do('iter_contacts', status, caps=CapContact)
def cb(self, cb, backend, contact):
if contact:
cb(contact)
elif not backend:
self.process = None
cb(None)
class ContactsWidget(QWidget):
def __init__(self, weboob, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_Contacts()
self.ui.setupUi(self)
self.weboob = weboob
self.contact = None
self.ui.contactList.setItemDelegate(HTMLDelegate())
self.url_process = None
self.photo_processes = {}
self.ui.groupBox.addItem('All', MetaGroup(self.weboob, 'all', self.tr('All')))
self.ui.groupBox.addItem('Online', MetaGroup(self.weboob, 'online', self.tr('Online')))
self.ui.groupBox.addItem('Offline', MetaGroup(self.weboob, 'offline', self.tr('Offline')))
self.ui.groupBox.setCurrentIndex(1)
self.connect(self.ui.groupBox, SIGNAL('currentIndexChanged(int)'), self.groupChanged)
self.connect(self.ui.contactList, SIGNAL('itemClicked(QListWidgetItem*)'), self.contactChanged)
self.connect(self.ui.refreshButton, SIGNAL('clicked()'), self.refreshContactList)
self.connect(self.ui.urlButton, SIGNAL('clicked()'), self.urlClicked)
def load(self):
self.refreshContactList()
self.ui.backendsList.clear()
for backend in self.weboob.iter_backends():
self.ui.backendsList.addItem(backend.name)
def groupChanged(self, i):
self.refreshContactList()
def refreshContactList(self):
self.ui.contactList.clear()
self.ui.refreshButton.setEnabled(False)
i = self.ui.groupBox.currentIndex()
group = self.ui.groupBox.itemData(i).toPyObject()
group.iter_contacts(self.addContact)
def setPhoto(self, contact, item):
if not contact:
return False
try:
self.photo_processes.pop(contact.id, None)
except KeyError:
pass
img = None
for photo in contact.photos.itervalues():
if photo.thumbnail_data:
img = QImage.fromData(photo.thumbnail_data)
break
if img:
item.setIcon(QIcon(QPixmap.fromImage(img)))
return True
return False
def addContact(self, contact):
if not contact:
self.ui.refreshButton.setEnabled(True)
return
status = ''
if contact.status == Contact.STATUS_ONLINE:
status = u'Online'
status_color = 0x00aa00
elif contact.status == Contact.STATUS_OFFLINE:
status = u'Offline'
status_color = 0xff0000
elif contact.status == Contact.STATUS_AWAY:
status = u'Away'
status_color = 0xffad16
else:
status = u'Unknown'
status_color = 0xaaaaaa
if contact.status_msg:
status += u' — %s' % contact.status_msg
item = QListWidgetItem()
item.setText('
%s
%s %s' % (contact.name, status_color, status, contact.backend))
item.setData(Qt.UserRole, contact)
if contact.photos is NotLoaded:
process = QtDo(self.weboob, lambda b, c: self.setPhoto(c, item))
process.do('fillobj', contact, ['photos'], backends=contact.backend)
self.photo_processes[contact.id] = process
elif len(contact.photos) > 0:
if not self.setPhoto(contact, item):
photo = contact.photos.values()[0]
process = QtDo(self.weboob, lambda b, p: self.setPhoto(contact, item))
process.do('fillobj', photo, ['thumbnail_data'], backends=contact.backend)
self.photo_processes[contact.id] = process
for i in xrange(self.ui.contactList.count()):
if self.ui.contactList.item(i).data(Qt.UserRole).toPyObject().status > contact.status:
self.ui.contactList.insertItem(i, item)
return
self.ui.contactList.addItem(item)
def contactChanged(self, current):
if not current:
return
contact = current.data(Qt.UserRole).toPyObject()
self.setContact(contact)
def setContact(self, contact):
if not contact or contact == self.contact:
return
if not isinstance(contact, Contact):
return self.retrieveContact(contact)
self.ui.tabWidget.clear()
self.contact = contact
backend = self.weboob.get_backend(self.contact.backend)
self.ui.tabWidget.addTab(ContactProfile(self.weboob, self.contact), self.tr('Profile'))
if backend.has_caps(CapMessages):
self.ui.tabWidget.addTab(ContactThread(self.weboob, self.contact, backend.has_caps(CapMessagesPost)), self.tr('Messages'))
if backend.has_caps(CapChat):
self.ui.tabWidget.setTabEnabled(self.ui.tabWidget.addTab(QWidget(), self.tr('Chat')),
False)
self.ui.tabWidget.setTabEnabled(self.ui.tabWidget.addTab(QWidget(), self.tr('Calendar')),
False)
self.ui.tabWidget.addTab(ContactNotes(self.weboob, self.contact), self.tr('Notes'))
def urlClicked(self):
url = unicode(self.ui.urlEdit.text())
if not url:
return
self.retrieveContact(url)
def retrieveContact(self, url):
backend_name = unicode(self.ui.backendsList.currentText())
self.ui.urlButton.setEnabled(False)
self.url_process = QtDo(self.weboob, self.retrieveContact_cb, self.retrieveContact_eb)
self.url_process.do('get_contact', url, backends=backend_name)
def retrieveContact_cb(self, backend, contact):
if not backend:
self.url_process = None
self.ui.urlButton.setEnabled(True)
return
self.ui.urlEdit.clear()
self.setContact(contact)
def retrieveContact_eb(self, backend, error, backtrace):
content = unicode(self.tr('Unable to get contact:\n%s\n')) % to_unicode(error)
if logging.root.level == logging.DEBUG:
content += u'\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while getting contact'),
content, QMessageBox.Ok)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/events.py 0000664 0000000 0000000 00000013335 12411220504 0030154 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QWidget, QTreeWidgetItem, QImage, QIcon, QPixmap
from PyQt4.QtCore import SIGNAL, Qt
from weboob.capabilities.base import NotLoaded
from weboob.tools.application.qt import QtDo, HTMLDelegate
from .ui.events_ui import Ui_Events
class EventsWidget(QWidget):
def __init__(self, weboob, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_Events()
self.ui.setupUi(self)
self.weboob = weboob
self.photo_processes = {}
self.event_filter = None
self.connect(self.ui.eventsList, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.eventDoubleClicked)
self.connect(self.ui.typeBox, SIGNAL('currentIndexChanged(int)'), self.typeChanged)
self.connect(self.ui.refreshButton, SIGNAL('clicked()'), self.refreshEventsList)
self.ui.eventsList.setItemDelegate(HTMLDelegate())
self.ui.eventsList.sortByColumn(1, Qt.DescendingOrder)
def load(self):
self.refreshEventsList()
def typeChanged(self, i):
if self.ui.refreshButton.isEnabled():
self.refreshEventsList()
def refreshEventsList(self):
self.ui.eventsList.clear()
self.ui.refreshButton.setEnabled(False)
if self.ui.typeBox.currentIndex() >= 0:
# XXX strangely, in gotEvent() in the loop to check if there is already the
# event type to try to introduce it in list, itemData() returns the right value.
# But, I don't know why, here, it will ALWAYS return None...
# So the filter does not work currently.
self.events_filter = self.ui.typeBox.itemData(self.ui.typeBox.currentIndex())
else:
self.event_filter = None
self.ui.typeBox.setEnabled(False)
self.ui.typeBox.clear()
self.ui.typeBox.addItem('All', None)
self.process = QtDo(self.weboob, self.gotEvent)
self.process.do('iter_events')
def setPhoto(self, contact, item):
if not contact:
return False
try:
self.photo_processes.pop(contact.id, None)
except KeyError:
pass
img = None
for photo in contact.photos.itervalues():
if photo.thumbnail_data:
img = QImage.fromData(photo.thumbnail_data)
break
if img:
item.setIcon(0, QIcon(QPixmap.fromImage(img)))
self.ui.eventsList.resizeColumnToContents(0)
return True
return False
def gotEvent(self, backend, event):
if not backend:
self.ui.refreshButton.setEnabled(True)
self.ui.typeBox.setEnabled(True)
return
found = False
for i in xrange(self.ui.typeBox.count()):
s = self.ui.typeBox.itemData(i)
if s == event.type:
found = True
if not found:
print event.type
self.ui.typeBox.addItem(event.type.capitalize(), event.type)
if event.type == self.event_filter:
self.ui.typeBox.setCurrentIndex(self.ui.typeBox.count()-1)
if self.event_filter and self.event_filter != event.type:
return
contact = event.contact
contact.backend = event.backend
status = ''
if contact.status == contact.STATUS_ONLINE:
status = u'Online'
status_color = 0x00aa00
elif contact.status == contact.STATUS_OFFLINE:
status = u'Offline'
status_color = 0xff0000
elif contact.status == contact.STATUS_AWAY:
status = u'Away'
status_color = 0xffad16
else:
status = u'Unknown'
status_color = 0xaaaaaa
if contact.status_msg:
status += u' — %s' % contact.status_msg
name = '
%s
%s %s' % (contact.name, status_color, status, event.backend)
date = event.date.strftime('%Y-%m-%d %H:%M')
type = event.type
message = event.message
item = QTreeWidgetItem(None, [name, date, type, message])
item.setData(0, Qt.UserRole, event)
if contact.photos is NotLoaded:
process = QtDo(self.weboob, lambda b, c: self.setPhoto(c, item))
process.do('fillobj', contact, ['photos'], backends=contact.backend)
self.photo_processes[contact.id] = process
elif len(contact.photos) > 0:
if not self.setPhoto(contact, item):
photo = contact.photos.values()[0]
process = QtDo(self.weboob, lambda b, p: self.setPhoto(contact, item))
process.do('fillobj', photo, ['thumbnail_data'], backends=contact.backend)
self.photo_processes[contact.id] = process
self.ui.eventsList.addTopLevelItem(item)
self.ui.eventsList.resizeColumnToContents(0)
self.ui.eventsList.resizeColumnToContents(1)
def eventDoubleClicked(self, item, col):
event = item.data(0, Qt.UserRole).toPyObject()
self.emit(SIGNAL('display_contact'), event.contact)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/main_window.py 0000664 0000000 0000000 00000006327 12411220504 0031166 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QWidget
from PyQt4.QtCore import SIGNAL
from weboob.tools.application.qt import QtMainWindow
from weboob.tools.application.qt.backendcfg import BackendCfg
from weboob.capabilities.dating import CapDating
try:
from weboob.applications.qboobmsg.messages_manager import MessagesManager
HAVE_BOOBMSG = True
except ImportError:
HAVE_BOOBMSG = False
from .ui.main_window_ui import Ui_MainWindow
from .status import AccountsStatus
from .contacts import ContactsWidget
from .events import EventsWidget
from .search import SearchWidget
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, parent=None):
QtMainWindow.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.loaded_tabs = {}
self.connect(self.ui.actionBackends, SIGNAL("triggered()"), self.backendsConfig)
self.connect(self.ui.tabWidget, SIGNAL('currentChanged(int)'), self.tabChanged)
self.addTab(AccountsStatus(self.weboob), self.tr('Status'))
self.addTab(MessagesManager(self.weboob) if HAVE_BOOBMSG else None, self.tr('Messages'))
self.addTab(ContactsWidget(self.weboob), self.tr('Contacts'))
self.addTab(EventsWidget(self.weboob), self.tr('Events'))
self.addTab(SearchWidget(self.weboob), self.tr('Search'))
self.addTab(None, self.tr('Calendar'))
self.addTab(None, self.tr('Optimizations'))
if self.weboob.count_backends() == 0:
self.backendsConfig()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapDating,), self)
if bckndcfg.run():
self.loaded_tabs.clear()
widget = self.ui.tabWidget.widget(self.ui.tabWidget.currentIndex())
widget.load()
def addTab(self, widget, title):
if widget:
self.connect(widget, SIGNAL('display_contact'), self.display_contact)
self.ui.tabWidget.addTab(widget, title)
else:
index = self.ui.tabWidget.addTab(QWidget(), title)
self.ui.tabWidget.setTabEnabled(index, False)
def tabChanged(self, i):
widget = self.ui.tabWidget.currentWidget()
if hasattr(widget, 'load') and not i in self.loaded_tabs:
widget.load()
self.loaded_tabs[i] = True
def display_contact(self, contact):
self.ui.tabWidget.setCurrentIndex(2)
widget = self.ui.tabWidget.currentWidget()
widget.setContact(contact)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/qhavedate.py 0000664 0000000 0000000 00000002665 12411220504 0030616 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.dating import CapDating
from weboob.tools.application.qt import QtApplication
from .main_window import MainWindow
class QHaveDate(QtApplication):
APPNAME = 'qhavedate'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2012 Romain Bignon'
DESCRIPTION = "Qt application allowing to interact with various dating websites."
SHORT_DESCRIPTION = "interact with dating websites"
CAPS = CapDating
STORAGE_FILENAME = 'dating.storage'
def main(self, argv):
self.create_storage(self.STORAGE_FILENAME)
self.load_backends(CapDating)
self.main_window = MainWindow(self.config, self.weboob)
self.main_window.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/search.py 0000664 0000000 0000000 00000006501 12411220504 0030112 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QWidget
from PyQt4.QtCore import SIGNAL
from weboob.tools.application.qt import QtDo
from .ui.search_ui import Ui_Search
from .contacts import ContactProfile
from .status import Account
class SearchWidget(QWidget):
def __init__(self, weboob, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_Search()
self.ui.setupUi(self)
self.weboob = weboob
self.contacts = []
self.accounts = []
self.current = None
self.connect(self.ui.nextButton, SIGNAL('clicked()'), self.next)
self.connect(self.ui.queryButton, SIGNAL('clicked()'), self.sendQuery)
def load(self):
while self.ui.statusFrame.layout().count() > 0:
item = self.ui.statusFrame.layout().takeAt(0)
if item.widget():
item.widget().deinit()
item.widget().hide()
item.widget().deleteLater()
self.accounts = []
for backend in self.weboob.iter_backends():
account = Account(self.weboob, backend)
account.title.setText(u'
%s
' % backend.name)
self.accounts.append(account)
self.ui.statusFrame.layout().addWidget(account)
self.ui.statusFrame.layout().addStretch()
self.getNewProfiles()
def updateStats(self):
for account in self.accounts:
account.updateStats()
def getNewProfiles(self):
self.newprofiles_process = QtDo(self.weboob, self.retrieveNewContacts_cb)
self.newprofiles_process.do('iter_new_contacts')
def retrieveNewContacts_cb(self, backend, contact):
if not backend:
return
self.contacts.insert(0, contact)
self.ui.queueLabel.setText('%d' % len(self.contacts))
if self.current is None:
self.next()
def next(self):
try:
contact = self.contacts.pop()
except IndexError:
contact = None
self.ui.queueLabel.setText('%d' % len(self.contacts))
self.setContact(contact)
self.updateStats()
def setContact(self, contact):
self.current = contact
if contact is not None:
widget = ContactProfile(self.weboob, contact)
self.ui.scrollArea.setWidget(widget)
else:
self.ui.scrollArea.setWidget(None)
def sendQuery(self):
self.newprofiles_process = QtDo(self.weboob, self.querySent)
self.newprofiles_process.do('send_query', self.current.id, backends=[self.current.backend])
def querySent(self, backend, query):
if backend is None:
self.next()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/status.py 0000664 0000000 0000000 00000010527 12411220504 0030173 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QScrollArea, QWidget, QHBoxLayout, QVBoxLayout, QFrame, QLabel, QImage, QPixmap
from weboob.capabilities.account import CapAccount, StatusField
from weboob.tools.application.qt import QtDo
from weboob.tools.misc import to_unicode
class Account(QFrame):
def __init__(self, weboob, backend, parent=None):
QFrame.__init__(self, parent)
self.setFrameShape(QFrame.StyledPanel)
self.setFrameShadow(QFrame.Raised)
self.weboob = weboob
self.backend = backend
self.setLayout(QVBoxLayout())
self.timer = None
head = QHBoxLayout()
headw = QWidget()
headw.setLayout(head)
self.title = QLabel(u'
%s — %s
' % (backend.name, backend.DESCRIPTION))
self.body = QLabel()
if backend.ICON:
self.icon = QLabel()
img = QImage(backend.ICON)
self.icon.setPixmap(QPixmap.fromImage(img))
head.addWidget(self.icon)
head.addWidget(self.title)
head.addStretch()
self.layout().addWidget(headw)
if backend.has_caps(CapAccount):
self.body.setText(u'Waiting...')
self.layout().addWidget(self.body)
self.timer = self.weboob.repeat(60, self.updateStats)
def deinit(self):
if self.timer is not None:
self.weboob.stop(self.timer)
def updateStats(self):
self.process = QtDo(self.weboob, self.updateStats_cb, self.updateStats_eb)
self.process.body = u''
self.process.in_p = False
self.process.do('get_account_status', backends=self.backend)
def updateStats_cb(self, backend, field):
if not field:
if self.process.in_p:
self.process.body += u"
"
self.body.setText(self.process.body)
self.process = None
return
if field.flags & StatusField.FIELD_HTML:
value = u'%s' % field.value
else:
value = (u'%s' % field.value).replace('&', '&').replace('<', '<').replace('>', '>')
if field.flags & StatusField.FIELD_TEXT:
if self.process.in_p:
self.process.body += u''
self.process.body += u'
%s
' % value
self.process.in_p = False
else:
if not self.process.in_p:
self.process.body += u"
"
self.process.in_p = True
else:
self.process.body += u" "
self.process.body += u'%s: %s' % (field.label, field.value)
def updateStats_eb(self, backend, err, backtrace):
self.body.setText(u'Unable to connect: %s' % to_unicode(err))
self.title.setText(u'%s' % unicode(self.title.text()))
class AccountsStatus(QScrollArea):
def __init__(self, weboob, parent=None):
QScrollArea.__init__(self, parent)
self.weboob = weboob
self.setFrameShadow(self.Plain)
self.setFrameShape(self.NoFrame)
self.setWidgetResizable(True)
widget = QWidget(self)
widget.setLayout(QVBoxLayout())
widget.show()
self.setWidget(widget)
def load(self):
while self.widget().layout().count() > 0:
item = self.widget().layout().takeAt(0)
if item.widget():
item.widget().deinit()
item.widget().hide()
item.widget().deleteLater()
for backend in self.weboob.iter_backends():
account = Account(self.weboob, backend)
self.widget().layout().addWidget(account)
self.widget().layout().addStretch()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/ 0000775 0000000 0000000 00000000000 12411220504 0026706 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/Makefile 0000664 0000000 0000000 00000000265 12411220504 0030351 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0031005 0 ustar 00root root 0000000 0000000 contact_thread.ui 0000664 0000000 0000000 00000011121 12411220504 0032144 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui
ContactThread00578429FormQt::Vertical00QFrame::StyledPanelQFrame::Raisedfalsefalse00Send050Qt::Horizontal4020../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png01Qt::ScrollBarAsNeededtrueQt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft00556154QWidget#scrollAreaContent {
background-color: rgb(255, 255, 255);
}
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/contacts.ui 0000664 0000000 0000000 00000007306 12411220504 0031071 0 ustar 00root root 0000000 0000000
Contacts00478374FormQt::Horizontal00QFrame::StyledPanelQFrame::Raised../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png1201201truefalsetrueFrom URLDisplay10
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/events.ui 0000664 0000000 0000000 00000004524 12411220504 0030556 0 ustar 00root root 0000000 0000000
Events00497318Form../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.png../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../usr/share/icons/oxygen/16x16/actions/view-refresh.pngQAbstractItemView::NoEditTriggerstrue6464falsetruetrueContactDateTypeMessage
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/main_window.ui0000664 0000000 0000000 00000004167 12411220504 0031570 0 ustar 00root root 0000000 0000000
MainWindow00763580QHaveDate-10076324FiletoolBarTopToolBarAreafalseBackendsQuitQuitactionQuittriggered()MainWindowclose()-1-1381289
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/notes.ui 0000664 0000000 0000000 00000001234 12411220504 0030375 0 ustar 00root root 0000000 0000000
Notes00430323FormSave
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/profile.ui 0000664 0000000 0000000 00000021210 12411220504 0030701 0 ustar 00root root 0000000 0000000
Profile00755647Form0QFrame::NoFrameQFrame::Plaintrue0075564700000QFrame::StyledPanelQFrame::Raised<h1>Loading...</h1>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse<b>URL:</b>trueQt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse0900Qt::Vertical200QFrame::StyledPanelQFrame::Raised002016777215<Qt::AlignCenter002016777215>Qt::AlignCenterQt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::Vertical130Qt::Horizontal00true20-1
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui/search.ui 0000664 0000000 0000000 00000006302 12411220504 0030513 0 ustar 00root root 0000000 0000000
Search00438349Form4030016777215QFrame::StyledPanelQFrame::Raised<b>Profiles in queue:</b>Qt::Horizontal4020QFrame::StyledPanelQFrame::RaisedQueryNexttrue00178235
thread_message.ui 0000664 0000000 0000000 00000005367 12411220504 0032154 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qhavedate/ui
ThreadMessage0055276FrameQFrame::StyledPanelQFrame::RaisedQt::Vertical20100trueQt::LinksAccessibleByMouse|Qt::TextSelectableByMousetrueQt::LinksAccessibleByMouse|Qt::TextSelectableByMouseQt::Vertical201
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/ 0000775 0000000 0000000 00000000000 12411220504 0026137 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/__init__.py 0000664 0000000 0000000 00000000067 12411220504 0030253 0 ustar 00root root 0000000 0000000 from .qvideoob import QVideoob
__all__ = ['QVideoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/main_window.py 0000664 0000000 0000000 00000012475 12411220504 0031035 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtCore import SIGNAL
from weboob.capabilities.video import CapVideo
from weboob.tools.application.qt import QtMainWindow, QtDo
from weboob.tools.application.qt.backendcfg import BackendCfg
from weboob.applications.qvideoob.ui.main_window_ui import Ui_MainWindow
from .video import Video
from .minivideo import MiniVideo
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
QtMainWindow.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.minivideos = []
self.app = app
self.ui.sortbyEdit.setCurrentIndex(int(self.config.get('settings', 'sortby')))
self.ui.nsfwCheckBox.setChecked(int(self.config.get('settings', 'nsfw')))
self.ui.sfwCheckBox.setChecked(int(self.config.get('settings', 'sfw')))
self.connect(self.ui.searchEdit, SIGNAL("returnPressed()"), self.search)
self.connect(self.ui.urlEdit, SIGNAL("returnPressed()"), self.openURL)
self.connect(self.ui.nsfwCheckBox, SIGNAL("stateChanged(int)"), self.nsfwChanged)
self.connect(self.ui.sfwCheckBox, SIGNAL("stateChanged(int)"), self.sfwChanged)
self.connect(self.ui.actionBackends, SIGNAL("triggered()"), self.backendsConfig)
self.loadBackendsList()
if self.ui.backendEdit.count() == 0:
self.backendsConfig()
def backendsConfig(self):
bckndcfg = BackendCfg(self.weboob, (CapVideo,), self)
if bckndcfg.run():
self.loadBackendsList()
def loadBackendsList(self):
self.ui.backendEdit.clear()
for i, backend in enumerate(self.weboob.iter_backends()):
if i == 0:
self.ui.backendEdit.addItem('All backends', '')
self.ui.backendEdit.addItem(backend.name, backend.name)
if backend.name == self.config.get('settings', 'backend'):
self.ui.backendEdit.setCurrentIndex(i+1)
if self.ui.backendEdit.count() == 0:
self.ui.searchEdit.setEnabled(False)
self.ui.urlEdit.setEnabled(False)
else:
self.ui.searchEdit.setEnabled(True)
self.ui.urlEdit.setEnabled(True)
def nsfwChanged(self, state):
self.config.set('settings', 'nsfw', int(self.ui.nsfwCheckBox.isChecked()))
self.updateVideosDisplay()
def sfwChanged(self, state):
self.config.set('settings', 'sfw', int(self.ui.sfwCheckBox.isChecked()))
self.updateVideosDisplay()
def updateVideosDisplay(self):
for minivideo in self.minivideos:
if (minivideo.video.nsfw and self.ui.nsfwCheckBox.isChecked() or
not minivideo.video.nsfw and self.ui.sfwCheckBox.isChecked()):
minivideo.show()
else:
minivideo.hide()
def search(self):
pattern = unicode(self.ui.searchEdit.text())
if not pattern:
return
for minivideo in self.minivideos:
self.ui.scrollAreaContent.layout().removeWidget(minivideo)
minivideo.hide()
minivideo.deleteLater()
self.minivideos = []
self.ui.searchEdit.setEnabled(False)
backend_name = str(self.ui.backendEdit.itemData(self.ui.backendEdit.currentIndex()).toString())
self.process = QtDo(self.weboob, self.addVideo)
self.process.do(self.app._do_complete, 20, (), 'search_videos', pattern, self.ui.sortbyEdit.currentIndex(), nsfw=True, backends=backend_name)
def addVideo(self, backend, video):
if not backend:
self.ui.searchEdit.setEnabled(True)
self.process = None
return
minivideo = MiniVideo(self.weboob, backend, video)
self.ui.scrollAreaContent.layout().addWidget(minivideo)
self.minivideos.append(minivideo)
if (video.nsfw and not self.ui.nsfwCheckBox.isChecked() or
not video.nsfw and not self.ui.sfwCheckBox.isChecked()):
minivideo.hide()
def openURL(self):
url = unicode(self.ui.urlEdit.text())
if not url:
return
for backend in self.weboob.iter_backends():
video = backend.get_video(url)
if video:
video_widget = Video(video, self)
video_widget.show()
self.ui.urlEdit.clear()
def closeEvent(self, ev):
self.config.set('settings', 'backend', str(self.ui.backendEdit.itemData(self.ui.backendEdit.currentIndex()).toString()))
self.config.set('settings', 'sortby', self.ui.sortbyEdit.currentIndex())
self.config.save()
ev.accept()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/minivideo.py 0000664 0000000 0000000 00000005012 12411220504 0030472 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QFrame, QImage, QPixmap
from weboob.tools.application.qt import QtDo
from weboob.applications.qvideoob.ui.minivideo_ui import Ui_MiniVideo
from .video import Video
class MiniVideo(QFrame):
def __init__(self, weboob, backend, video, parent=None):
QFrame.__init__(self, parent)
self.ui = Ui_MiniVideo()
self.ui.setupUi(self)
self.weboob = weboob
self.backend = backend
self.video = video
self.ui.titleLabel.setText(video.title)
self.ui.backendLabel.setText(backend.name)
self.ui.durationLabel.setText(unicode(video.duration))
self.ui.authorLabel.setText(unicode(video.author))
self.ui.dateLabel.setText(video.date and unicode(video.date) or '')
if video.rating_max:
self.ui.ratingLabel.setText('%s / %s' % (video.rating, video.rating_max))
else:
self.ui.ratingLabel.setText('%s' % video.rating)
self.process_thumbnail = QtDo(self.weboob, self.gotThumbnail)
self.process_thumbnail.do('fillobj', self.video, ['thumbnail'], backends=backend)
def gotThumbnail(self, backend, video):
if not backend:
return
if video.thumbnail and video.thumbnail.data:
img = QImage.fromData(video.thumbnail.data)
self.ui.imageLabel.setPixmap(QPixmap.fromImage(img))
def enterEvent(self, event):
self.setFrameShadow(self.Sunken)
QFrame.enterEvent(self, event)
def leaveEvent(self, event):
self.setFrameShadow(self.Raised)
QFrame.leaveEvent(self, event)
def mousePressEvent(self, event):
QFrame.mousePressEvent(self, event)
video = self.backend.fillobj(self.video)
if video:
video_widget = Video(video, self)
video_widget.show()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/qvideoob.py 0000664 0000000 0000000 00000003077 12411220504 0030330 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.video import CapVideo
from weboob.tools.application.qt import QtApplication
from .main_window import MainWindow
class QVideoob(QtApplication):
APPNAME = 'qvideoob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Romain Bignon'
DESCRIPTION = "Qt application allowing to search videos on various websites and play them."
SHORT_DESCRIPTION = "search and play videos"
CAPS = CapVideo
CONFIG = {'settings': {'nsfw': 1,
'sfw': 1,
'sortby': 0,
'backend': ''
}
}
def main(self, argv):
self.load_backends(CapVideo)
self.load_config()
self.main_window = MainWindow(self.config, self.weboob, self)
self.main_window.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/ui/ 0000775 0000000 0000000 00000000000 12411220504 0026554 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/ui/Makefile 0000664 0000000 0000000 00000000265 12411220504 0030217 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/ui/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0030653 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/ui/main_window.ui 0000664 0000000 0000000 00000012410 12411220504 0031424 0 ustar 00root root 0000000 0000000
MainWindow00582463QVideoobQFrame::StyledPanelQFrame::RaisedSearch: 00RelevanceRatingDurationDate10000Display:SFWtrueNSFWtrueQt::Horizontal4020true00560230QFrame::StyledPanelQFrame::RaisedURL: 0058225toolBarTopToolBarAreafalseBackends
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/ui/minivideo.ui 0000664 0000000 0000000 00000011337 12411220504 0031103 0 ustar 00root root 0000000 0000000
MiniVideo00464132FormQFrame::StyledPanelQFrame::Raised95500QFormLayout::ExpandingFieldsGrow75trueTitle0050truefalseTextLabel75trueDurationTextLabel75trueAuthorTextLabel75trueDateTextLabel75trueRatingTextLabel75trueWhereTextLabel
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/ui/video.ui 0000664 0000000 0000000 00000013515 12411220504 0030226 0 ustar 00root root 0000000 0000000
Video00647404Video1275trueQFrame::BoxQFrame::RaisedTextLabelQt::AlignCenter00QFrame::StyledPanelQFrame::Sunken00truetrueQFrame::StyledPanelQFrame::RaisedQFormLayout::ExpandingFieldsGrow75trueURLtrueArrowCursortruefalsetrue75trueDurationTextLabel75trueAuthorTextLabel75trueDateTextLabel75trueRatingTextLabelPhonon::VideoPlayerQWidgetphonon/videoplayer.hPhonon::SeekSliderQWidgetphonon/seekslider.h
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qvideoob/video.py 0000664 0000000 0000000 00000003657 12411220504 0027632 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtCore import QUrl
from PyQt4.QtGui import QDialog
from PyQt4.phonon import Phonon
from weboob.applications.qvideoob.ui.video_ui import Ui_Video
class Video(QDialog):
def __init__(self, video, parent=None):
QDialog.__init__(self, parent)
self.ui = Ui_Video()
self.ui.setupUi(self)
self.video = video
self.setWindowTitle("Video - %s" % video.title)
self.ui.urlEdit.setText(video.url)
self.ui.titleLabel.setText(video.title)
self.ui.durationLabel.setText(unicode(video.duration))
self.ui.authorLabel.setText(unicode(video.author))
self.ui.dateLabel.setText(unicode(video.date))
if video.rating_max:
self.ui.ratingLabel.setText('%s / %s' % (video.rating, video.rating_max))
else:
self.ui.ratingLabel.setText('%s' % video.rating)
self.ui.seekSlider.setMediaObject(self.ui.videoPlayer.mediaObject())
self.ui.videoPlayer.load(Phonon.MediaSource(QUrl(video.url)))
self.ui.videoPlayer.play()
def closeEvent(self, event):
self.ui.videoPlayer.stop()
event.accept()
def hideEvent(self, event):
self.ui.videoPlayer.stop()
event.accept()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qwebcontentedit/ 0000775 0000000 0000000 00000000000 12411220504 0027526 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qwebcontentedit/__init__.py0000664 0000000 0000000 00000000115 12411220504 0031634 0 ustar 00root root 0000000 0000000 from .qwebcontentedit import QWebContentEdit
__all__ = ['QWebContentEdit']
main_window.py 0000664 0000000 0000000 00000025170 12411220504 0032341 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qwebcontentedit # -*- coding: utf-8 -*-
# Copyright(C) 2011 Clément Schreiner
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import logging
from copy import deepcopy
from PyQt4.QtCore import SIGNAL
from PyQt4.QtGui import QMessageBox, QTableWidgetItem
from PyQt4.QtCore import Qt
from weboob.tools.application.base import MoreResultsAvailable
from weboob.tools.application.qt import QtMainWindow, QtDo
from weboob.tools.application.qt.backendcfg import BackendCfg
from weboob.capabilities.content import CapContent
from weboob.tools.misc import to_unicode
from .ui.main_window_ui import Ui_MainWindow
class MainWindow(QtMainWindow):
def __init__(self, config, weboob, app, parent=None):
QtMainWindow.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.config = config
self.weboob = weboob
self.backend = None
self.app = app
self.connect(self.ui.idEdit,
SIGNAL("returnPressed()"),
self.loadPage)
self.connect(self.ui.loadButton,
SIGNAL("clicked()"),
self.loadPage)
self.connect(self.ui.tabWidget,
SIGNAL("currentChanged(int)"),
self._currentTabChanged)
self.connect(self.ui.saveButton,
SIGNAL("clicked()"),
self.savePage)
self.connect(self.ui.actionBackends,
SIGNAL("triggered()"),
self.backendsConfig)
self.connect(self.ui.contentEdit,
SIGNAL("textChanged()"),
self._textChanged)
self.connect(self.ui.loadHistoryButton,
SIGNAL("clicked()"),
self.loadHistory)
if hasattr(self.ui.descriptionEdit, "setPlaceholderText"):
self.ui.descriptionEdit.setPlaceholderText("Edit summary")
if self.weboob.count_backends() == 0:
self.backendsConfig()
else:
self.loadBackends()
def backendsConfig(self):
""" Opens backends configuration dialog when 'Backends' is clicked """
bckndcfg = BackendCfg(self.weboob, (CapContent,), self)
if bckndcfg.run():
self.loadBackends()
def loadBackends(self):
""" Fills the backends comboBox with available backends """
self.ui.backendBox.clear()
for backend in self.weboob.iter_backends():
self.ui.backendBox.insertItem(0, backend.name)
def _currentTabChanged(self):
""" Loads history or preview when the corresponding tabs are shown """
if self.ui.tabWidget.currentIndex() == 1:
if self.backend is not None:
self.loadPreview()
elif self.ui.tabWidget.currentIndex() == 2:
if self.backend is not None:
self.loadHistory()
def _textChanged(self):
""" The text in the content QPlainTextEdit has changed """
if self.backend:
self.ui.saveButton.setEnabled(True)
self.ui.saveButton.setText('Save')
def loadPage(self):
""" Loads a page's source into the 'content' QPlainTextEdit """
_id = unicode(self.ui.idEdit.text())
if not _id:
return
self.ui.loadButton.setEnabled(False)
self.ui.loadButton.setText('Loading...')
self.ui.contentEdit.setReadOnly(True)
backend = str(self.ui.backendBox.currentText())
self.process = QtDo(self.weboob,
self._loadedPage,
self._errorLoadPage)
self.process.do('get_content', _id, backends=(backend,))
def _loadedPage(self, backend, data):
""" Callback for loadPage """
if not backend:
# Loading is finished
self.process = None
if self.backend:
self.ui.contentEdit.setReadOnly(False)
self.ui.loadButton.setEnabled(True)
self.ui.loadButton.setText('Load')
return
if not data:
self.content = None
self.backend = None
QMessageBox.critical(self, self.tr('Unable to open page'),
'Unable to open page "%s" on %s: it does not exist.'
% (self.ui.idEdit.text(),
self.ui.backendBox.currentText()),
QMessageBox.Ok)
return
self.content = data
self.ui.contentEdit.setPlainText(self.content.content)
self.setWindowTitle("QWebcontentedit - %s@%s" %(self.content.id,
backend.name))
self.backend = backend
def _errorLoadPage(self, backend, error, backtrace):
""" Error callback for loadPage """
content = unicode(self.tr('Unable to load page:\n%s\n')) % to_unicode(error)
if logging.root.level == logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while loading page'),
content, QMessageBox.Ok)
self.ui.loadButton.setEnabled(True)
self.ui.loadButton.setText("Load")
def savePage(self):
""" Saves the current page to the remote site """
if self.backend is None:
return
new_content = unicode(self.ui.contentEdit.toPlainText())
minor = self.ui.minorBox.isChecked()
if new_content != self.content.content:
self.ui.saveButton.setEnabled(False)
self.ui.saveButton.setText('Saving...')
self.ui.contentEdit.setReadOnly(True)
self.content.content = new_content
message = unicode(self.ui.descriptionEdit.text())
self.process = QtDo(self.weboob,
self._savedPage,
self._errorSavePage)
self.process.do('push_content',
self.content,
message,
minor=minor,
backends=self.backend)
def _savedPage(self, backend, data):
""" Callback for savePage's QtDo """
if not backend:
# saving has finished
self.process = None
self.ui.saveButton.setText('Saved')
self.ui.contentEdit.setReadOnly(False)
return
self.ui.descriptionEdit.clear()
def _errorSavePage(self, backend, error, backtrace):
""" """
content = unicode(self.tr('Unable to save page:\n%s\n')) % to_unicode(error)
if logging.root.level == logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while saving page'),
content, QMessageBox.Ok)
self.ui.saveButton.setEnabled(True)
self.ui.saveButton.setText("Save")
def loadPreview(self):
""" Loads the current page's preview into the preview QTextEdit """
tmp_content = deepcopy(self.content)
tmp_content.content = unicode(self.ui.contentEdit.toPlainText())
self.ui.previewEdit.setHtml(self.backend.get_content_preview(tmp_content))
def loadHistory(self):
""" Loads the page's log into the 'History' tab """
if self.backend is None:
return
self.ui.loadHistoryButton.setEnabled(False)
self.ui.loadHistoryButton.setText("Loading...")
self.ui.historyTable.clear()
self.ui.historyTable.setRowCount(0)
self.ui.historyTable.setHorizontalHeaderLabels(["Revision",
"Time",
"Author",
"Summary"])
self.ui.historyTable.setColumnWidth(3, 1000)
self.process = QtDo(self.weboob,
self._gotRevision,
self._errorHistory)
self.process.do(self.app._do_complete,
self.ui.nbRevBox.value(),
(),
'iter_revisions',
self.content.id,
backends=(self.backend,))
def _gotRevision(self, backend, revision):
""" Callback for loadHistory's QtDo """
if not backend:
# The last revision has been processed
self.process = None
self.ui.loadHistoryButton.setEnabled(True)
self.ui.loadHistoryButton.setText("Reload")
return
# we set the flags to Qt.ItemIsEnabled so that the items
# are not modifiable (they are modifiable by default)
item_revision = QTableWidgetItem(revision.id)
item_revision.setFlags(Qt.ItemIsEnabled)
item_time = QTableWidgetItem(revision.timestamp.strftime('%Y-%m-%d %H:%M:%S'))
item_time.setFlags(Qt.ItemIsEnabled)
item_author = QTableWidgetItem(revision.author)
item_author.setFlags(Qt.ItemIsEnabled)
item_summary = QTableWidgetItem(revision.comment)
item_summary.setFlags(Qt.ItemIsEnabled)
row = self.ui.historyTable.currentRow() + 1
self.ui.historyTable.insertRow(row)
self.ui.historyTable.setItem(row, 0, item_revision)
self.ui.historyTable.setItem(row, 1, item_time)
self.ui.historyTable.setItem(row, 2, item_author)
self.ui.historyTable.setItem(row, 3, item_summary)
self.ui.historyTable.setCurrentCell(row, 0)
def _errorHistory(self, backend, error, backtrace):
""" Loading the history has failed """
if isinstance(error, MoreResultsAvailable):
return
content = unicode(self.tr('Unable to load history:\n%s\n')) % to_unicode(error)
if logging.root.level == logging.DEBUG:
content += '\n%s\n' % to_unicode(backtrace)
QMessageBox.critical(self, self.tr('Error while loading history'),
content, QMessageBox.Ok)
self.ui.loadHistoryButton.setEnabled(True)
self.ui.loadHistoryButton.setText("Reload")
qwebcontentedit.py 0000664 0000000 0000000 00000002606 12411220504 0033224 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qwebcontentedit # -*- coding: utf-8 -*-
# Copyright(C) 2011 Clément Schreiner
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.application.qt import QtApplication
from weboob.capabilities.content import CapContent
from .main_window import MainWindow
class QWebContentEdit(QtApplication):
APPNAME = 'qwebcontentedit'
VERSION = '1.0'
COPYRIGHT = u'Copyright(C) 2011 Clément Schreiner'
DESCRIPTION = "Qt application allowing to manage content of various websites."
SHORT_DESCRIPTION = "manage websites content"
CAPS = CapContent
def main(self, argv):
self.load_backends(CapContent, storage=self.create_storage())
self.main_window = MainWindow(self.config, self.weboob, self)
self.main_window.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qwebcontentedit/ui/ 0000775 0000000 0000000 00000000000 12411220504 0030143 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qwebcontentedit/ui/Makefile0000664 0000000 0000000 00000000265 12411220504 0031606 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
__init__.py 0000664 0000000 0000000 00000000000 12411220504 0032163 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qwebcontentedit/ui main_window.ui 0000664 0000000 0000000 00000014055 12411220504 0032743 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qwebcontentedit/ui
MainWindow00608609QWebcontenteditfalseLoad0EdittruePreviewtrueHistoryNew ColumnTimeAuthorSummaryQt::Horizontal4020Number of revisions10ReloadMinor editfalseNo changes0060822FiletoolBarTopToolBarAreafalseExitBackendsactionExittriggered()MainWindowclose()-1-1343306
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qweboobcfg/ 0000775 0000000 0000000 00000000000 12411220504 0026445 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qweboobcfg/__init__.py 0000664 0000000 0000000 00000001435 12411220504 0030561 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .qweboobcfg import QWeboobCfg
__all__ = ['QWeboobCfg']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/qweboobcfg/qweboobcfg.py 0000664 0000000 0000000 00000002553 12411220504 0031142 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.application.qt import BackendCfg, QtApplication
class QWeboobCfg(QtApplication):
APPNAME = 'qweboobcfg'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Romain Bignon'
DESCRIPTION = "weboob-config-qt is a graphical application to add/edit/remove backends, " \
"and to register new website accounts."
SHORT_DESCRIPTION = "manage backends or register new accounts"
def main(self, argv):
self.load_backends()
self.dlg = BackendCfg(self.weboob)
self.dlg.show()
return self.weboob.loop()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/radioob/ 0000775 0000000 0000000 00000000000 12411220504 0025746 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/radioob/__init__.py 0000664 0000000 0000000 00000001424 12411220504 0030060 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .radioob import Radioob
__all__ = ['Radioob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/radioob/radioob.py 0000664 0000000 0000000 00000035604 12411220504 0027747 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import subprocess
import os
import re
import requests
from weboob.capabilities.radio import CapRadio, Radio
from weboob.capabilities.audio import CapAudio, BaseAudio, Playlist, Album
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.media_player import InvalidMediaPlayer, MediaPlayer, MediaPlayerNotFound
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Radioob']
class RadioListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
result = ''
if hasattr(obj, 'description') and not empty(obj.description):
result += '%-30s' % obj.description
if hasattr(obj, 'current') and not empty(obj.current):
if obj.current.who:
result += ' (Current: %s - %s)' % (obj.current.who, obj.current.what)
else:
result += ' (Current: %s)' % obj.current.what
return result
class SongListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
result = obj.title
if hasattr(obj, 'author') and not empty(obj.author):
result += ' (%s)' % obj.author
return result
def get_description(self, obj):
result = ''
if hasattr(obj, 'description') and not empty(obj.description):
result += '%-30s' % obj.description
return result
class AlbumTrackListInfoFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'tracks_list')
def get_title(self, obj):
result = obj.title
if hasattr(obj, 'author') and not empty(obj.author):
result += ' (%s)' % obj.author
return result
def get_description(self, obj):
result = ''
for song in obj.tracks_list:
result += '- %s%-30s%s ' % (self.BOLD, song.title, self.NC)
if hasattr(song, 'duration') and not empty(song.duration):
result += '%-10s ' % song.duration
else:
result += '%-10s ' % ' '
result += '(%s)\r\n\t' % (song.id)
return result
class PlaylistTrackListInfoFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'tracks_list')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
result = ''
for song in obj.tracks_list:
result += '- %s%-30s%s ' % (self.BOLD, song.title, self.NC)
if hasattr(song, 'author') and not empty(song.author):
result += '(%-15s) ' % song.author
if hasattr(song, 'duration') and not empty(song.duration):
result += '%-10s ' % song.duration
else:
result += '%-10s ' % ' '
result += '(%s)\r\n\t' % (song.id)
return result
class Radioob(ReplApplication):
APPNAME = 'radioob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2013 Romain Bignon\nCopyright(C) 2013 Pierre Maziere'
DESCRIPTION = "Console application allowing to search for web radio stations, listen to them and get information " \
"like the current song."
SHORT_DESCRIPTION = "search, show or listen to radio stations"
CAPS = (CapRadio, CapAudio)
EXTRA_FORMATTERS = {'radio_list': RadioListFormatter,
'song_list': SongListFormatter,
'album_tracks_list_info': AlbumTrackListInfoFormatter,
'playlist_tracks_list_info': PlaylistTrackListInfoFormatter,
}
COMMANDS_FORMATTERS = {'ls': 'radio_list',
'playlist': 'radio_list',
}
COLLECTION_OBJECTS = (Radio, BaseAudio, )
PLAYLIST = []
def __init__(self, *args, **kwargs):
ReplApplication.__init__(self, *args, **kwargs)
self.player = MediaPlayer(self.logger)
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def complete_download(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_download(self, line):
"""
download ID [FILENAME]
Download an audio file
"""
_id, dest = self.parse_command_args(line, 2, 1)
audio = self.get_object(_id, 'get_audio', ['url'])
if not audio:
print >>self.stderr, 'Audio file not found: %s' % _id
return 3
if not audio.url:
print >>self.stderr, 'Error: the direct URL is not available.'
return 4
def check_exec(executable):
with open('/dev/null', 'w') as devnull:
process = subprocess.Popen(['which', executable], stdout=devnull)
if process.wait() != 0:
print >>self.stderr, 'Please install "%s"' % executable
return False
return True
def audio_to_file(_audio):
ext = _audio.ext
if not ext:
ext = 'audiofile'
return '%s.%s' % (re.sub('[?:/]', '-', _audio.id), ext)
if dest is not None and os.path.isdir(dest):
dest += '/%s' % audio_to_file(audio)
if dest is None:
dest = audio_to_file(audio)
if audio.url.startswith('rtmp'):
if not check_exec('rtmpdump'):
return 1
args = ('rtmpdump', '-e', '-r', audio.url, '-o', dest)
elif audio.url.startswith('mms'):
if not check_exec('mimms'):
return 1
args = ('mimms', '-r', audio.url, dest)
else:
if check_exec('wget'):
args = ('wget', '-c', audio.url, '-O', dest)
elif check_exec('curl'):
args = ('curl', '-C', '-', audio.url, '-o', dest)
else:
return 1
os.spawnlp(os.P_WAIT, args[0], *args)
def complete_play(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_play(self, line):
"""
play ID [stream_id]
Play a radio or a audio file with a found player (optionnaly specify the wanted stream).
"""
_id, stream_id = self.parse_command_args(line, 2, 1)
if not _id:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('play', short=True)
return 2
try:
stream_id = int(stream_id)
except (ValueError, TypeError):
stream_id = 0
obj = self.retrieve_obj(_id)
if obj is None:
print >>self.stderr, 'No object matches with this id:', _id
return 3
if isinstance(obj, Radio):
try:
streams = [obj.streams[stream_id]]
except IndexError:
print >>self.stderr, 'Stream %d not found' % stream_id
return 1
elif isinstance(obj, BaseAudio):
streams = [obj]
else:
streams = obj.tracks_list
if len(streams) == 0:
print >>self.stderr, 'Radio or Audio file not found:', _id
return 3
try:
player_name = self.config.get('media_player')
media_player_args = self.config.get('media_player_args')
if not player_name:
self.logger.debug(u'You can set the media_player key to the player you prefer in the radioob '
'configuration file.')
for stream in streams:
if isinstance(stream, BaseAudio) and not stream.url:
stream = self.get_object(stream.id, 'get_audio')
else:
r = requests.get(stream.url, stream=True)
buf = r.iter_content(512).next()
r.close()
playlistFormat = None
for line in buf.split("\n"):
if playlistFormat is None:
if line == "[playlist]":
playlistFormat = "pls"
elif line == "#EXTM3U":
playlistFormat = "m3u"
else:
break
elif playlistFormat == "pls":
if line.startswith('File'):
stream.url = line.split('=', 1).pop(1).strip()
break
elif playlistFormat == "m3u":
if line[0] != "#":
stream.url = line.strip()
break
self.player.play(stream, player_name=player_name, player_args=media_player_args)
except (InvalidMediaPlayer, MediaPlayerNotFound) as e:
print '%s\nRadio URL: %s' % (e, stream.url)
def retrieve_obj(self, _id):
obj = None
if self.interactive:
try:
obj = self.objects[int(_id) - 1]
_id = obj.id
except (IndexError, ValueError):
pass
m = CapAudio.get_object_method(_id)
if m:
obj = self.get_object(_id, m)
return obj if obj is not None else self.get_object(_id, 'get_radio')
def do_playlist(self, line):
"""
playlist cmd [args]
playlist add ID [ID2 ID3 ...]
playlist remove ID [ID2 ID3 ...]
playlist export [FILENAME]
playlist display
"""
if not line:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('playlist')
return 2
cmd, args = self.parse_command_args(line, 2, req_n=1)
if cmd == "add":
_ids = args.strip().split(' ')
for _id in _ids:
audio = self.get_object(_id, 'get_audio')
if not audio:
print >>self.stderr, 'Audio file not found: %s' % _id
return 3
if not audio.url:
print >>self.stderr, 'Error: the direct URL is not available.'
return 4
self.PLAYLIST.append(audio)
elif cmd == "remove":
_ids = args.strip().split(' ')
for _id in _ids:
audio_to_remove = self.get_object(_id, 'get_audio')
if not audio_to_remove:
print >>self.stderr, 'Audio file not found: %s' % _id
return 3
if not audio_to_remove.url:
print >>self.stderr, 'Error: the direct URL is not available.'
return 4
for audio in self.PLAYLIST:
if audio.id == audio_to_remove.id:
self.PLAYLIST.remove(audio)
break
elif cmd == "export":
filename = "playlist.m3u"
if args:
filename = args
file = open(filename, 'w')
for audio in self.PLAYLIST:
file.write('%s\r\n' % audio.url)
file.close()
elif cmd == "display":
for audio in self.PLAYLIST:
self.cached_format(audio)
else:
print >>self.stderr, 'Playlist command only support "add", "remove", "display" and "export" arguments.'
return 2
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about a radio or an audio file.
"""
if not _id:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True)
return 2
obj = self.retrieve_obj(_id)
if isinstance(obj, Album):
self.set_formatter('album_tracks_list_info')
elif isinstance(obj, Playlist):
self.set_formatter('playlist_tracks_list_info')
if obj is None:
print >>self.stderr, 'No object matches with this id:', _id
return 3
self.format(obj)
@defaultcount(10)
def do_search(self, pattern=None):
"""
search (radio|song|album|playlist) PATTERN
List (radio|song|album|playlist) matching a PATTERN.
If PATTERN is not given, this command will list all the (radio|song|album|playlist).
"""
if not pattern:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('playlist')
return 2
cmd, args = self.parse_command_args(pattern, 2, req_n=1)
if not args:
args = ""
self.set_formatter_header(u'Search pattern: %s' % pattern if pattern else u'All radios')
self.change_path([u'search'])
if cmd == "radio":
self.set_formatter('radio_list')
for backend, radio in self.do('iter_radios_search', pattern=args):
self.add_object(radio)
self.format(radio)
elif cmd == "song":
self.set_formatter('song_list')
for backend, audio in self.do('search_audio', pattern=args):
self.add_object(audio)
self.format(audio)
elif cmd == "album":
self.set_formatter('song_list')
for backend, album in self.do('search_album', pattern=args):
self.add_object(album)
self.format(album)
elif cmd == "playlist":
self.set_formatter('song_list')
for backend, playlist in self.do('search_playlist', pattern=args):
self.add_object(playlist)
self.format(playlist)
else:
print >>self.stderr, 'Search command only supports "radio", "song", "album" and "playlist" arguments.'
return 2
def do_ls(self, line):
"""
ls
List radios
"""
ret = super(Radioob, self).do_ls(line)
return ret
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/suboob/ 0000775 0000000 0000000 00000000000 12411220504 0025620 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/suboob/__init__.py 0000664 0000000 0000000 00000001415 12411220504 0027732 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .suboob import Suboob
__all__ = ['Suboob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/suboob/suboob.py 0000664 0000000 0000000 00000017004 12411220504 0027465 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.subtitle import CapSubtitle
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['Suboob']
LANGUAGE_CONV = {
'ar': 'ara', 'eo': 'epo', 'ga': '', 'ru': 'rus',
'af': '', 'et': 'est', 'it': 'ita', 'sr': 'scc',
'sq': 'alb', 'tl': '', 'ja': 'jpn', 'sk': 'slo',
'hy': 'arm', 'fi': 'fin', 'kn': '', 'sl': 'slv',
'az': '', 'fr': 'fre', 'ko': 'kor', 'es': 'spa',
'eu': 'baq', 'gl': 'glg', 'la': '', 'sw': 'swa',
'be': '', 'ka': 'geo', 'lv': 'lav', 'sv': 'swe',
'bn': 'ben', 'de': 'ger', 'lt': 'lit', 'ta': '',
'bg': 'bul', 'gr': 'ell', 'mk': 'mac', 'te': 'tel',
'ca': 'cat', 'gu': '', 'ms': 'may', 'th': 'tha',
'zh': 'chi', 'ht': '', 'mt': '', 'tr': 'tur',
'hr': 'hrv', 'iw': 'heb', 'no': 'nor', 'uk': 'ukr',
'cz': 'cze', 'hi': 'hin', 'fa': 'per', 'ur': 'urd',
'da': 'dan', 'hu': 'hun', 'pl': 'pol', 'vi': 'vie',
'nl': 'dut', 'is': 'ice', 'pt': 'por', 'cy': '',
'en': 'eng', 'id': 'ind', 'ro': 'rum', 'yi': ''}
def sizeof_fmt(num):
for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
if num < 1024.0:
return "%-4.1f%s" % (num, x)
num /= 1024.0
class SubtitleInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'name', 'url', 'description')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.name, self.NC)
result += 'ID: %s\n' % obj.fullid
result += 'URL: %s\n' % obj.url
if not empty(obj.language):
result += 'LANG: %s\n' % obj.language
if not empty(obj.nb_cd):
result += 'NB CD: %s\n' % obj.nb_cd
result += '\n%sDescription%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.description
return result
class SubtitleListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name')
def get_title(self, obj):
return obj.name
def get_description(self, obj):
result = u'lang : %s' % obj.language
result += ' ; %s CD' % obj.nb_cd
if not empty(obj.url):
result += ' ; url : %s' % obj.url
return result
class Suboob(ReplApplication):
APPNAME = 'suboob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2013 Julien Veyssier'
DESCRIPTION = "Console application allowing to search for subtitles on various services " \
"and download them."
SHORT_DESCRIPTION = "search and download subtitles"
CAPS = CapSubtitle
EXTRA_FORMATTERS = {'subtitle_list': SubtitleListFormatter,
'subtitle_info': SubtitleInfoFormatter
}
COMMANDS_FORMATTERS = {'search': 'subtitle_list',
'info': 'subtitle_info'
}
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, id):
"""
info ID
Get information about a subtitle.
"""
subtitle = self.get_object(id, 'get_subtitle')
if not subtitle:
print >>self.stderr, 'Subtitle not found: %s' % id
return 3
self.start_format()
self.format(subtitle)
def complete_download(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_download(self, line):
"""
download ID [FILENAME]
Get the subtitle or archive file.
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
subtitle = self.get_object(id, 'get_subtitle')
if not subtitle:
print >>self.stderr, 'Subtitle not found: %s' % id
return 3
if dest is None:
ext = subtitle.ext
if empty(ext):
ext = 'zip'
dest = '%s.%s' % (subtitle.name, ext)
for backend, buf in self.do('get_subtitle_file', subtitle.id, backends=subtitle.backend):
if buf:
if dest == '-':
self.stdout.write(buf)
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print >>self.stderr, 'Unable to write file in "%s": %s' % (dest, e)
return 1
else:
print 'Saved to %s' % dest
return
@defaultcount(10)
def do_search(self, line):
"""
search language [PATTERN]
Search subtitles.
Language Abbreviation
----------------------
Arabic ar Esperanto eo Irish ga Russian ru
Afrikaans af Estonian et Italian it Serbian sr
Albanian sq Filipino tl Japanese ja Slovak sk
Armenian hy Finnish fi Kannada kn Slovenian sl
Azerbaijani az French fr Korean ko Spanish es
Basque eu Galician gl Latin la Swahili sw
Belarusian be Georgian ka Latvian lv Swedish sv
Bengali bn German de Lithuanian lt Tamil ta
Bulgarian bg Greek gr Macedonian mk Telugu te
Catalan ca Gujarati gu Malay ms Thai th
Chinese zh Haitian ht Maltese mt Turkish tr
Croatian hr Hebrew iw Norwegian no Ukrainian uk
Czech cz Hindi hi Persian fa Urdu ur
Danish da Hungaric hu Polish pl Vietnamese vi
Dutch nl Icelandic is Portuguese pt Welsh cy
English en Indonesian id Romanian ro Yiddish yi
----------------------
"""
language, pattern = self.parse_command_args(line, 2, 1)
self.change_path([u'search'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, subtitle in self.do('iter_subtitles', language=language, pattern=pattern):
self.cached_format(subtitle)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/translaboob/ 0000775 0000000 0000000 00000000000 12411220504 0026635 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/translaboob/__init__.py 0000664 0000000 0000000 00000001434 12411220504 0030750 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Lucien Loiseau
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .translaboob import Translaboob
__all__ = ['Translaboob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/translaboob/translaboob.py 0000664 0000000 0000000 00000013341 12411220504 0031517 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Lucien Loiseau
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.translate import CapTranslate, TranslationFail, LanguageNotSupported
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['Translaboob']
class TranslationFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'text')
def format_obj(self, obj, alias):
return u'%s* %s%s\n\t%s' % (self.BOLD, obj.backend, self.NC, obj.text.replace('\n', '\n\t'))
class XmlTranslationFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'text')
def start_format(self, **kwargs):
if 'source' in kwargs:
self.output('' % kwargs['source'])
def format_obj(self, obj, alias):
return u'\n%s\n' % (obj.backend, obj.text)
class Translaboob(ReplApplication):
APPNAME = 'translaboob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2012 Lucien Loiseau'
DESCRIPTION = "Console application to translate text from one language to another"
SHORT_DESCRIPTION = "translate text from one language to another"
CAPS = CapTranslate
EXTRA_FORMATTERS = {'translation': TranslationFormatter,
'xmltrans': XmlTranslationFormatter,
}
COMMANDS_FORMATTERS = {'translate': 'translation',
}
LANGUAGE = {
'ar': 'Arabic', 'af': 'Afrikaans', 'sq': 'Albanian', 'hy': 'Armenian', 'az': 'Azerbaijani', 'eu': 'Basque', 'be': 'Belarusian',
'bn': 'Bengali', 'bg': 'Bulgarian', 'ca': 'Catalan', 'zh': 'Chinese', 'hr': 'Croatian', 'cz': 'Czech', 'da': 'Danish',
'nl': 'Dutch', 'en': 'English', 'eo': 'Esperanto', 'et': 'Estonian', 'tl': 'Filipino', 'fi': 'Finnish', 'fr': 'French',
'gl': 'Galician', 'ka': 'Georgian', 'de': 'German', 'gr': 'Greek', 'gu': 'Gujarati', 'ht': 'Haitian', 'iw': 'Hebrew',
'hi': 'Hindi', 'hu': 'Hungaric', 'is': 'Icelandic', 'id': 'Indonesian', 'ga': 'Irish', 'it': 'Italian', 'ja': 'Japanese',
'kn': 'Kannada', 'ko': 'Korean', 'la': 'Latin', 'lv': 'Latvian', 'lt': 'Lithuanian', 'mk': 'Macedonian', 'ms': 'Malay',
'mt': 'Maltese', 'no': 'Norwegian', 'fa': 'Persian', 'pl': 'Polish', 'pt': 'Portuguese', 'ro': 'Romanian', 'ru': 'Russian',
'sr': 'Serbian', 'sk': 'Slovak', 'sl': 'Slovenian', 'es': 'Spanish', 'sw': 'Swahili', 'sv': 'Swedish', 'ta': 'Tamil',
'te': 'Telugu', 'th': 'Thai', 'tr': 'Turkish', 'uk': 'Ukrainian', 'ur': 'Urdu', 'vi': 'Vietnamese', 'cy': 'Welsh', 'yi': 'Yiddish',
'nigger': 'Nigger!',
}
def do_translate(self, line):
"""
translate FROM TO [TEXT]
Translate from one language to another.
* FROM : source language
* TO : destination language
* TEXT : language to translate, standard input if - is given
Language Abbreviation
----------------------
Arabic ar Esperanto eo Irish ga Russian ru
Afrikaans af Estonian et Italian it Serbian sr
Albanian sq Filipino tl Japanese ja Slovak sk
Armenian hy Finnish fi Kannada kn Slovenian sl
Azerbaijani az French fr Korean ko Spanish es
Basque eu Galician gl Latin la Swahili sw
Belarusian be Georgian ka Latvian lv Swedish sv
Bengali bn German de Lithuanian lt Tamil ta
Bulgarian bg Greek gr Macedonian mk Telugu te
Catalan ca Gujarati gu Malay ms Thai th
Chinese zh Haitian ht Maltese mt Turkish tr
Croatian hr Hebrew iw Norwegian no Ukrainian uk
Czech cz Hindi hi Persian fa Urdu ur
Danish da Hungaric hu Polish pl Vietnamese vi
Dutch nl Icelandic is Portuguese pt Welsh cy
English en Indonesian id Romanian ro Yiddish yi
----------------------
"""
lan_from, lan_to, text = self.parse_command_args(line, 3, 2)
try:
if not lan_from in self.LANGUAGE.keys():
raise LanguageNotSupported()
if not lan_to in self.LANGUAGE.keys():
raise LanguageNotSupported()
if not text or text == '-':
text = self.acquire_input()
self.start_format(source=text)
for backend, translation in self.do('translate', self.LANGUAGE[lan_from], self.LANGUAGE[lan_to], text):
self.format(translation)
except (TranslationFail, LanguageNotSupported) as error:
print >>self.stderr, error
pass
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/traveloob/ 0000775 0000000 0000000 00000000000 12411220504 0026324 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/traveloob/__init__.py 0000664 0000000 0000000 00000001536 12411220504 0030442 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .traveloob import Traveloob
__all__ = ['Traveloob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/traveloob/traveloob.py 0000664 0000000 0000000 00000015103 12411220504 0030673 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Romain Bignon, Julien Hébert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import datetime
from weboob.capabilities.base import Currency, empty
from weboob.capabilities.travel import CapTravel, RoadmapFilters
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Traveloob']
class DeparturesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'type', 'departure_station', 'arrival_station', 'time')
def get_title(self, obj):
s = obj.type
if hasattr(obj, 'price') and not empty(obj.price):
s += u' %s %s' % (self.colored(u'—', 'cyan'), self.colored('%6.2f %s' % (obj.price, Currency.currency2txt(obj.currency)), 'green'))
if hasattr(obj, 'late') and not empty(obj.late) and obj.late > datetime.time():
s += u' %s %s' % (self.colored(u'—', 'cyan'), self.colored('Late: %s' % obj.late, 'red', 'bold'))
if hasattr(obj, 'information') and not empty(obj.information) and obj.information.strip() != '':
s += u' %s %s' % (self.colored(u'—', 'cyan'), self.colored(obj.information, 'red'))
return s
def get_description(self, obj):
if hasattr(obj, 'arrival_time') and not empty(obj.arrival_time):
s = '(%s) %s%s\n\t(%s) %s' % (self.colored(obj.time.strftime('%H:%M'), 'cyan'),
obj.departure_station,
self.colored(' [Platform: %s]' % obj.platform, 'yellow') if (hasattr(obj, 'platform') and not empty(obj.platform)) else '',
self.colored(obj.arrival_time.strftime('%H:%M'), 'cyan'),
obj.arrival_station)
else:
s = '(%s) %20s -> %s' % (self.colored(obj.time.strftime('%H:%M'), 'cyan'),
obj.departure_station, obj.arrival_station)
return s
class StationsFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name')
def get_title(self, obj):
return obj.name
class Traveloob(ReplApplication):
APPNAME = 'traveloob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2013 Romain Bignon'
DESCRIPTION = "Console application allowing to search for train stations and get departure times."
SHORT_DESCRIPTION = "search for train stations and departures"
CAPS = CapTravel
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {'stations': StationsFormatter,
'departures': DeparturesFormatter,
}
COMMANDS_FORMATTERS = {'stations': 'stations',
'departures': 'departures',
}
def add_application_options(self, group):
group.add_option('--departure-time')
group.add_option('--arrival-time')
@defaultcount(10)
def do_stations(self, pattern):
"""
stations PATTERN
Search stations.
"""
for backend, station in self.do('iter_station_search', pattern):
self.format(station)
@defaultcount(10)
def do_departures(self, line):
"""
departures STATION [ARRIVAL [DATE]]]
List all departures for a given station.
The format for the date is "yyyy-mm-dd HH:MM" or "HH:MM".
"""
station, arrival, date = self.parse_command_args(line, 3, 1)
station_id, backend_name = self.parse_id(station)
if arrival:
arrival_id, backend_name2 = self.parse_id(arrival)
if backend_name and backend_name2 and backend_name != backend_name2:
print >>self.stderr, 'Departure and arrival aren\'t on the same backend'
return 1
else:
arrival_id = backend_name2 = None
if backend_name:
backends = [backend_name]
elif backend_name2:
backends = [backend_name2]
else:
backends = None
if date is not None:
try:
date = self.parse_datetime(date)
except ValueError as e:
print >>self.stderr, 'Invalid datetime value: %s' % e
print >>self.stderr, 'Please enter a datetime in form "yyyy-mm-dd HH:MM" or "HH:MM".'
return 1
for backend, departure in self.do('iter_station_departures', station_id, arrival_id, date, backends=backends):
self.format(departure)
def do_roadmap(self, line):
"""
roadmap DEPARTURE ARRIVAL
Display the roadmap to travel from DEPARTURE to ARRIVAL.
Command-line parameters:
--departure-time TIME requested departure time
--arrival-time TIME requested arrival time
TIME might be in form "yyyy-mm-dd HH:MM" or "HH:MM".
Example:
> roadmap Puteaux Aulnay-sous-Bois --arrival-time 22:00
"""
departure, arrival = self.parse_command_args(line, 2, 2)
filters = RoadmapFilters()
try:
filters.departure_time = self.parse_datetime(self.options.departure_time)
filters.arrival_time = self.parse_datetime(self.options.arrival_time)
except ValueError as e:
print >>self.stderr, 'Invalid datetime value: %s' % e
print >>self.stderr, 'Please enter a datetime in form "yyyy-mm-dd HH:MM" or "HH:MM".'
return 1
for backend, route in self.do('iter_roadmap', departure, arrival, filters):
self.format(route)
def parse_datetime(self, text):
if text is None:
return None
try:
date = datetime.datetime.strptime(text, '%Y-%m-%d %H:%M')
except ValueError:
try:
date = datetime.datetime.strptime(text, '%H:%M')
except ValueError:
raise ValueError(text)
date = datetime.datetime.now().replace(hour=date.hour, minute=date.minute)
return date
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/videoob/ 0000775 0000000 0000000 00000000000 12411220504 0025756 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/videoob/__init__.py 0000664 0000000 0000000 00000001426 12411220504 0030072 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .videoob import Videoob
__all__ = ['Videoob']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/videoob/videoob.py 0000664 0000000 0000000 00000026743 12411220504 0027773 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz, Romain Bignon, John Obbele, Nicolas Duhamel
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import requests
import subprocess
import os
from weboob.capabilities.video import CapVideo, BaseVideo
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.media_player import InvalidMediaPlayer, MediaPlayer, MediaPlayerNotFound
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Videoob']
class VideoListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'duration', 'date')
DISPLAYED_FIELDS = ('author', 'rating')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
if empty(obj.duration) and empty(obj.date):
return None
result = '%s' % (obj.duration or obj.date)
if hasattr(obj, 'author') and not empty(obj.author):
result += u' - %s' % obj.author
if hasattr(obj, 'rating') and not empty(obj.rating):
result += u' (%s/%s)' % (obj.rating, obj.rating_max)
return result
class Videoob(ReplApplication):
APPNAME = 'videoob'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Christophe Benz, Romain Bignon, John Obbele'
DESCRIPTION = "Console application allowing to search for videos on various websites, " \
"play and download them and get information."
SHORT_DESCRIPTION = "search and play videos"
CAPS = CapVideo
EXTRA_FORMATTERS = {'video_list': VideoListFormatter}
COMMANDS_FORMATTERS = {'search': 'video_list',
'ls': 'video_list',
'playlist': 'video_list'}
COLLECTION_OBJECTS = (BaseVideo, )
PLAYLIST = []
nsfw = True
def __init__(self, *args, **kwargs):
ReplApplication.__init__(self, *args, **kwargs)
self.player = MediaPlayer(self.logger)
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def download(self, video, dest, default=None):
if not video.url:
print >>self.stderr, 'Error: the direct URL is not available.'
return 4
def check_exec(executable):
with open('/dev/null', 'w') as devnull:
process = subprocess.Popen(['which', executable], stdout=devnull)
if process.wait() != 0:
print >>self.stderr, 'Please install "%s"' % executable
return False
return True
dest = self.obj_to_filename(video, dest, default)
if video.url.startswith('rtmp'):
if not check_exec('rtmpdump'):
return 1
args = ('rtmpdump', '-e', '-r', video.url, '-o', dest)
elif video.url.startswith('mms'):
if not check_exec('mimms'):
return 1
args = ('mimms', '-r', video.url, dest)
elif u'm3u8' == video.ext:
_dest, _ = os.path.splitext(dest)
dest = u'%s.%s' % (_dest, 'mp4')
content = tuple()
baseurl = video.url.rpartition('/')[0]
for line in self.read_url(video.url):
if not line.startswith('#'):
if not line.startswith('http'):
line = u'%s/%s' % (baseurl, line)
content += (line,)
args = ('wget',) + content + ('-O', dest)
else:
if check_exec('wget'):
args = ('wget', '-c', video.url, '-O', dest)
elif check_exec('curl'):
args = ('curl', '-C', '-', video.url, '-o', dest)
else:
return 1
os.spawnlp(os.P_WAIT, args[0], *args)
def read_url(self, url):
r = requests.get(url, stream=True)
buf = r.iter_lines()
r.close()
return buf
def complete_download(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_download(self, line):
"""
download ID [FILENAME]
Download a video
Braces-enclosed tags are replaced with data fields. Use the 'info'
command to see what fields are available on a given video.
Example: download KdRRge4XYIo@youtube '{title}.{ext}'
"""
_id, dest = self.parse_command_args(line, 2, 1)
video = self.get_object(_id, 'get_video', ['url'])
if not video:
print >>self.stderr, 'Video not found: %s' % _id
return 3
return self.download(video, dest)
def complete_play(self, text, line, *ignored):
args = line.split(' ')
if len(args) >= 2:
return self._complete_object()
def do_play(self, line):
"""
play ID
Play a video with a found player.
"""
if not line:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('play', short=True)
return 2
ret = 0
for _id in line.split(' '):
video = self.get_object(_id, 'get_video', ['url'])
error = self.play(video, _id)
if error is not None:
ret = error
return ret
def play(self, video, _id):
if not video:
print >>self.stderr, 'Video not found: %s' % _id
return 3
if not video.url:
print >>self.stderr, 'Error: the direct URL is not available.'
return 4
try:
player_name = self.config.get('media_player')
media_player_args = self.config.get('media_player_args')
if not player_name:
self.logger.info(u'You can set the media_player key to the player you prefer in the videoob '
'configuration file.')
self.player.play(video, player_name=player_name, player_args=media_player_args)
except (InvalidMediaPlayer, MediaPlayerNotFound) as e:
print '%s\nVideo URL: %s' % (e, video.url)
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) >= 2:
return self._complete_object()
def do_info(self, line):
"""
info ID [ID2 [...]]
Get information about a video.
"""
if not line:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True)
return 2
self.start_format()
for _id in line.split(' '):
video = self.get_object(_id, 'get_video')
if not video:
print >>self.stderr, 'Video not found: %s' % _id
return 3
self.format(video)
def complete_playlist(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return ['play', 'add', 'remove', 'export', 'display', 'download']
if len(args) >= 3:
if args[1] in ('export', 'download'):
return self.path_completer(args[2])
if args[1] in ('add', 'remove'):
return self._complete_object()
def do_playlist(self, line):
"""
playlist cmd [args]
playlist add ID [ID2 ID3 ...]
playlist remove ID [ID2 ID3 ...]
playlist export [FILENAME]
playlist display
playlist download [PATH]
playlist play
"""
if not self.interactive:
print >>self.stderr, 'This command can be used only in interactive mode.'
return 1
if not line:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('playlist')
return 2
cmd, args = self.parse_command_args(line, 2, req_n=1)
if cmd == "add":
_ids = args.strip().split(' ')
for _id in _ids:
video = self.get_object(_id, 'get_video')
if not video:
print >>self.stderr, 'Video not found: %s' % _id
return 3
if not video.url:
print >>self.stderr, 'Error: the direct URL is not available.'
return 4
self.PLAYLIST.append(video)
elif cmd == "remove":
_ids = args.strip().split(' ')
for _id in _ids:
video_to_remove = self.get_object(_id, 'get_video')
if not video_to_remove:
print >>self.stderr, 'Video not found: %s' % _id
return 3
if not video_to_remove.url:
print >>self.stderr, 'Error: the direct URL is not available.'
return 4
for video in self.PLAYLIST:
if video.id == video_to_remove.id:
self.PLAYLIST.remove(video)
break
elif cmd == "export":
filename = "playlist.m3u"
if args:
filename = args
file = open(filename, 'w')
for video in self.PLAYLIST:
file.write('%s\r\n' % video.url)
file.close()
elif cmd == "display":
for video in self.PLAYLIST:
self.cached_format(video)
elif cmd == "download":
for i, video in enumerate(self.PLAYLIST):
self.download(video, args, '%02d-{id}-{title}.{ext}' % (i+1))
elif cmd == "play":
for video in self.PLAYLIST:
self.play(video, video.id)
else:
print >>self.stderr, 'Playlist command only support "add", "remove", "display", "download" and "export" arguments.'
return 2
def complete_nsfw(self, text, line, begidx, endidx):
return ['on', 'off']
def do_nsfw(self, line):
"""
nsfw [on | off]
If argument is given, enable or disable the non-suitable for work behavior.
If no argument is given, print the current behavior.
"""
line = line.strip()
if line:
if line == 'on':
self.nsfw = True
elif line == 'off':
self.nsfw = False
else:
print 'Invalid argument "%s".' % line
return 2
else:
print "on" if self.nsfw else "off"
@defaultcount()
def do_search(self, pattern):
"""
search PATTERN
Search for videos matching a PATTERN.
"""
if not pattern:
print >>self.stderr, 'This command takes an argument: %s' % self.get_command_help('search', short=True)
return 2
self.change_path([u'search'])
self.start_format(pattern=pattern)
for backend, video in self.do('search_videos', pattern=pattern, nsfw=self.nsfw):
self.cached_format(video)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/webcontentedit/ 0000775 0000000 0000000 00000000000 12411220504 0027345 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/webcontentedit/__init__.py 0000664 0000000 0000000 00000001451 12411220504 0031457 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .webcontentedit import WebContentEdit
__all__ = ['WebContentEdit']
webcontentedit.py 0000664 0000000 0000000 00000016405 12411220504 0032664 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/webcontentedit # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import tempfile
import codecs
from distutils.spawn import find_executable
from weboob.core.bcall import CallErrors
from weboob.capabilities.content import CapContent, Revision
from weboob.tools.application.repl import ReplApplication, defaultcount
__all__ = ['WebContentEdit']
class WebContentEdit(ReplApplication):
APPNAME = 'webcontentedit'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Romain Bignon'
DESCRIPTION = "Console application allowing to display and edit contents on various websites."
SHORT_DESCRIPTION = "manage websites content"
CAPS = CapContent
def do_edit(self, line):
"""
edit ID [ID...]
Edit a content with $EDITOR, then push it on the website.
"""
contents = []
for id in line.split():
_id, backend_name = self.parse_id(id, unique_backend=True)
backend_names = (backend_name,) if backend_name is not None else self.enabled_backends
contents += [content for backend, content in self.do('get_content', _id, backends=backend_names) if content]
if len(contents) == 0:
print >>self.stderr, 'No contents found'
return 3
if self.stdin.isatty():
paths = {}
for content in contents:
tmpdir = os.path.join(tempfile.gettempdir(), "weboob")
if not os.path.isdir(tmpdir):
os.makedirs(tmpdir)
with tempfile.NamedTemporaryFile(prefix='%s_' % content.id.replace(os.path.sep, '_'), dir=tmpdir, delete=False) as f:
data = content.content
if isinstance(data, unicode):
data = data.encode('utf-8')
elif data is None:
content.content = u''
data = ''
f.write(data)
paths[f.name.encode('utf-8')] = content
params = ''
editor = os.environ.get('EDITOR', 'vim')
# check cases where /usr/bin/vi is a symlink to vim
if 'vim' in (os.path.basename(editor), os.path.basename(os.path.realpath(find_executable(editor) or '/')).replace('.nox', '')):
params = '-p'
os.system("%s %s %s" % (editor, params, ' '.join(['"%s"' % path.replace('"', '\\"') for path in paths.iterkeys()])))
for path, content in paths.iteritems():
with open(path, 'r') as f:
data = f.read()
try:
data = data.decode('utf-8')
except UnicodeError:
pass
if content.content != data:
content.content = data
else:
contents.remove(content)
if len(contents) == 0:
print >>self.stderr, 'No changes. Abort.'
return 1
print 'Contents changed:\n%s' % ('\n'.join(' * %s' % content.id for content in contents))
message = self.ask('Enter a commit message', default='')
minor = self.ask('Is this a minor edit?', default=False)
if not self.ask('Do you want to push?', default=True):
return
errors = CallErrors([])
for content in contents:
path = [path for path, c in paths.iteritems() if c == content][0]
self.stdout.write('Pushing %s...' % content.id.encode('utf-8'))
self.stdout.flush()
try:
self.do('push_content', content, message, minor=minor, backends=[content.backend]).wait()
except CallErrors as e:
errors.errors += e.errors
self.stdout.write(' error (content saved in %s)\n' % path)
else:
self.stdout.write(' done\n')
os.unlink(path)
else:
# stdin is not a tty
if len(contents) != 1:
print >>self.stderr, "Multiple ids not supported with pipe"
return 2
message, minor = '', False
data = self.stdin.read()
contents[0].content = data.decode(self.guess_encoding(self.stdin))
errors = CallErrors([])
for content in contents:
self.stdout.write('Pushing %s...' % content.id.encode(self.encoding))
self.stdout.flush()
try:
self.do('push_content', content, message, minor=minor, backends=[content.backend]).wait()
except CallErrors as e:
errors.errors += e.errors
self.stdout.write(' error\n')
else:
self.stdout.write(' done\n')
if len(errors.errors) > 0:
raise errors
@defaultcount(10)
def do_log(self, line):
"""
log ID
Display log of a page
"""
if not line:
print >>self.stderr, 'Error: please give a page ID'
return 2
_id, backend_name = self.parse_id(line)
backend_names = (backend_name,) if backend_name is not None else self.enabled_backends
_id = _id.encode('utf-8')
self.start_format()
for backend, revision in self.do('iter_revisions', _id, backends=backend_names):
self.format(revision)
def do_get(self, line):
"""
get ID [-r revision]
Get page contents
"""
if not line:
print >>self.stderr, 'Error: please give a page ID'
return 2
_part_line = line.strip().split(' ')
revision = None
if '-r' in _part_line:
r_index = _part_line.index('-r')
if len(_part_line) -1 > r_index:
revision = Revision(_part_line[r_index+1])
_part_line.remove(revision.id)
_part_line.remove('-r')
if not _part_line:
print >>self.stderr, 'Error: please give a page ID'
return 2
_id, backend_name = self.parse_id(" ".join(_part_line))
backend_names = (backend_name,) if backend_name is not None else self.enabled_backends
_id = _id.encode('utf-8')
output = codecs.getwriter(self.encoding)(self.stdout)
for contents in [content for backend, content in self.do('get_content', _id, revision, backends=backend_names) if content]:
output.write(contents.content)
# add a newline unless we are writing
# in a file or in a pipe
if os.isatty(output.fileno()):
output.write('\n')
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobcfg/ 0000775 0000000 0000000 00000000000 12411220504 0026264 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobcfg/__init__.py 0000664 0000000 0000000 00000001432 12411220504 0030375 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboobcfg import WeboobCfg
__all__ = ['WeboobCfg']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobcfg/weboobcfg.py 0000664 0000000 0000000 00000021346 12411220504 0030601 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import re
from weboob.capabilities.account import CapAccount
from weboob.core.modules import ModuleLoadError
from weboob.tools.application.repl import ReplApplication
from weboob.tools.ordereddict import OrderedDict
__all__ = ['WeboobCfg']
class WeboobCfg(ReplApplication):
APPNAME = 'weboob-config'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2012 Christophe Benz, Romain Bignon'
DESCRIPTION = "Weboob-Config is a console application to add/edit/remove backends, " \
"and to register new website accounts."
SHORT_DESCRIPTION = "manage backends or register new accounts"
COMMANDS_FORMATTERS = {'modules': 'table',
'list': 'table',
}
DISABLE_REPL = True
def load_default_backends(self):
pass
def do_add(self, line):
"""
add NAME [OPTIONS ...]
Add a backend.
"""
if not line:
print >>self.stderr, 'You must specify a module name. Hint: use the "modules" command.'
return 2
name, options = self.parse_command_args(line, 2, 1)
if options:
options = options.split(' ')
else:
options = ()
params = {}
# set backend params from command-line arguments
for option in options:
try:
key, value = option.split('=', 1)
except ValueError:
print >>self.stderr, 'Parameters have to be formatted "key=value"'
return 2
params[key] = value
self.add_backend(name, params)
def do_register(self, line):
"""
register MODULE
Register a new account on a module.
"""
self.register_backend(line)
def do_confirm(self, backend_name):
"""
confirm BACKEND
For a backend which support CapAccount, parse a confirmation mail
after using the 'register' command to automatically confirm the
subscribe.
It takes mail from stdin. Use it with postfix for example.
"""
# Do not use the ReplApplication.load_backends() method because we
# don't want to prompt user to create backend.
self.weboob.load_backends(names=[backend_name])
try:
backend = self.weboob.get_backend(backend_name)
except KeyError:
print >>self.stderr, 'Error: backend "%s" not found.' % backend_name
return 1
if not backend.has_caps(CapAccount):
print >>self.stderr, 'Error: backend "%s" does not support accounts management' % backend_name
return 1
mail = self.acquire_input()
if not backend.confirm_account(mail):
print >>self.stderr, 'Error: Unable to confirm account creation'
return 1
return 0
def do_list(self, line):
"""
list [CAPS ..]
Show backends.
"""
caps = line.split()
for instance_name, name, params in sorted(self.weboob.backends_config.iter_backends()):
try:
module = self.weboob.modules_loader.get_or_load_module(name)
except ModuleLoadError as e:
self.logger.warning('Unable to load module %r: %s' % (name, e))
continue
if caps and not module.has_caps(*caps):
continue
row = OrderedDict([('Name', instance_name),
('Module', name),
('Configuration', ', '.join(
'%s=%s' % (key, ('*****' if key in module.config and module.config[key].masked
else value))
for key, value in params.iteritems())),
])
self.format(row)
def do_remove(self, instance_name):
"""
remove NAME
Remove a backend.
"""
if not self.weboob.backends_config.remove_backend(instance_name):
print >>self.stderr, 'Backend instance "%s" does not exist' % instance_name
return 1
def _do_toggle(self, name, state):
try:
bname, items = self.weboob.backends_config.get_backend(name)
except KeyError:
print >>self.stderr, 'Backend instance "%s" does not exist' % name
return 1
self.weboob.backends_config.edit_backend(name, bname, {'_enabled': state})
def do_enable(self, name):
"""
enable BACKEND
Enable a disabled backend
"""
self._do_toggle(name, 1)
def do_disable(self, name):
"""
disable BACKEND
Disable a backend
"""
self._do_toggle(name, 0)
def do_edit(self, line):
"""
edit BACKEND
Edit a backend
"""
try:
self.edit_backend(line)
except KeyError:
print >>self.stderr, 'Error: backend "%s" not found' % line
return 1
def do_modules(self, line):
"""
modules [CAPS ...]
Show available modules.
"""
caps = line.split()
for name, info in sorted(self.weboob.repositories.get_all_modules_info(caps).iteritems()):
row = OrderedDict([('Name', name),
('Capabilities', ', '.join(info.capabilities)),
('Description', info.description),
])
self.format(row)
def do_info(self, line):
"""
info NAME
Display information about a module.
"""
if not line:
print >>self.stderr, 'You must specify a module name. Hint: use the "modules" command.'
return 2
minfo = self.weboob.repositories.get_module_info(line)
if not minfo:
print >>self.stderr, 'Module "%s" does not exist.' % line
return 1
try:
module = self.weboob.modules_loader.get_or_load_module(line)
except ModuleLoadError:
module = None
print '.------------------------------------------------------------------------------.'
print '| Module %-69s |' % minfo.name
print "+-----------------.------------------------------------------------------------'"
print '| Version | %s' % minfo.version
print '| Maintainer | %s' % minfo.maintainer
print '| License | %s' % minfo.license
print '| Description | %s' % minfo.description
print '| Capabilities | %s' % ', '.join(minfo.capabilities)
print '| Installed | %s%s' % (('yes' if module else 'no'), ' (new version available)' if self.weboob.repositories.versions.get(minfo.name) > minfo.version else '')
print '| Location | %s' % (minfo.url or os.path.join(minfo.path, minfo.name))
if module:
first = True
for key, field in module.config.iteritems():
value = field.label
if not field.default is None:
value += ' (default: %s)' % field.default
if first:
print '| | '
print '| Configuration | %s: %s' % (key, value)
first = False
else:
print '| | %s: %s' % (key, value)
print "'-----------------'"
def do_applications(self, line):
"""
applications
Show applications.
"""
applications = set()
import weboob.applications
for path in weboob.applications.__path__:
regexp = re.compile('^%s/([\w\d_]+)$' % path)
for root, dirs, files in os.walk(path):
m = regexp.match(root)
if m and '__init__.py' in files:
applications.add(m.group(1))
print ' '.join(sorted(applications)).encode('utf-8')
def do_update(self, line):
"""
update
Update weboob.
"""
self.weboob.update()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobcli/ 0000775 0000000 0000000 00000000000 12411220504 0026274 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobcli/__init__.py 0000664 0000000 0000000 00000001432 12411220504 0030405 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboobcli import WeboobCli
__all__ = ['WeboobCli']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobcli/weboobcli.py 0000664 0000000 0000000 00000003352 12411220504 0030616 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.application.repl import ReplApplication
__all__ = ['WeboobCli']
class WeboobCli(ReplApplication):
APPNAME = 'weboob-cli'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Romain Bignon'
SYNOPSIS = 'Usage: %prog [-dqv] [-b backends] [-cnfs] capability method [arguments..]\n'
SYNOPSIS += ' %prog [--help] [--version]'
DESCRIPTION = "Weboob-Cli is a console application to call a specific method on backends " \
"which implement the given capability."
SHORT_DESCRIPTION = "call a method on backends"
DISABLE_REPL = True
def load_default_backends(self):
pass
def main(self, argv):
if len(argv) < 3:
print >>self.stderr, "Syntax: %s capability method [args ..]" % argv[0]
return 2
cap_s = argv[1]
cmd = argv[2]
args = argv[3:]
self.load_backends(cap_s)
for backend, obj in self.do(cmd, *args):
self.format(obj)
self.flush()
return 0
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobdebug/ 0000775 0000000 0000000 00000000000 12411220504 0026613 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobdebug/__init__.py 0000664 0000000 0000000 00000001441 12411220504 0030724 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboobdebug import WeboobDebug
__all__ = ['WeboobDebug']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobdebug/weboobdebug.py 0000664 0000000 0000000 00000006466 12411220504 0031465 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from optparse import OptionGroup
from weboob.tools.application.base import Application
class WeboobDebug(Application):
APPNAME = 'weboobdebug'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2011 Christophe Benz'
DESCRIPTION = "Weboob-Debug is a console application to debug backends."
SHORT_DESCRIPTION = "debug backends"
def __init__(self, option_parser=None):
super(WeboobDebug, self).__init__(option_parser)
options = OptionGroup(self._parser, 'Weboob-Debug options')
options.add_option('-B', '--bpython', action='store_true', help='Prefer bpython over ipython')
self._parser.add_option_group(options)
def load_default_backends(self):
pass
def main(self, argv):
"""
BACKEND
Debug BACKEND.
"""
try:
backend_name = argv[1]
except IndexError:
print >>self.stderr, 'Usage: %s BACKEND' % argv[0]
return 1
try:
backend = self.weboob.load_backends(names=[backend_name])[backend_name]
except KeyError:
print >>self.stderr, u'Unable to load backend "%s"' % backend_name
return 1
locs = dict(backend=backend, browser=backend.browser, application=self, weboob=self.weboob)
banner = 'Weboob debug shell\nBackend "%s" loaded.\nAvailable variables:\n' % backend_name \
+ '\n'.join([' %s: %s' % (k, v) for k, v in locs.iteritems()])
if self.options.bpython:
funcs = [self.bpython, self.ipython, self.python]
else:
funcs = [self.ipython, self.bpython, self.python]
for func in funcs:
try:
func(locs, banner)
except ImportError:
continue
else:
break
def ipython(self, locs, banner):
try:
from IPython import embed
embed(user_ns=locs, banner2=banner)
except ImportError:
from IPython.Shell import IPShellEmbed
shell = IPShellEmbed(argv=[])
shell.set_banner(shell.IP.BANNER + '\n\n' + banner)
shell(local_ns=locs, global_ns={})
def bpython(self, locs, banner):
from bpython import embed
embed(locs, banner=banner)
def python(self, locs, banner):
import code
try:
import readline
import rlcompleter
readline.set_completer(rlcompleter.Completer(locs).complete)
readline.parse_and_bind("tab:complete")
except ImportError:
pass
code.interact(banner=banner, local=locs)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobrepos/ 0000775 0000000 0000000 00000000000 12411220504 0026655 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobrepos/__init__.py 0000664 0000000 0000000 00000001440 12411220504 0030765 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboobrepos import WeboobRepos
__all__ = ['WeboobRepos']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboobrepos/weboobrepos.py 0000664 0000000 0000000 00000021435 12411220504 0031562 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from datetime import datetime
from time import mktime, strptime
import tarfile
import os
import shutil
import subprocess
from copy import copy
from contextlib import closing
from weboob.core.repositories import Repository
from weboob.tools.application.repl import ReplApplication
__all__ = ['WeboobRepos']
class WeboobRepos(ReplApplication):
APPNAME = 'weboob-repos'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2012 Romain Bignon'
DESCRIPTION = "Weboob-repos is a console application to manage a Weboob Repository."
SHORT_DESCRIPTION = "manage a weboob repository"
COMMANDS_FORMATTERS = {'backends': 'table',
'list': 'table',
}
DISABLE_REPL = True
weboob_commands = copy(ReplApplication.weboob_commands)
weboob_commands.remove('backends')
def load_default_backends(self):
pass
def do_create(self, line):
"""
create NAME [PATH]
Create a new repository. If PATH is missing, create repository
on the current directory.
"""
name, path = self.parse_command_args(line, 2, 1)
if not path:
path = os.getcwd()
else:
path = os.path.realpath(path)
if not os.path.exists(path):
os.mkdir(path)
elif not os.path.isdir(path):
print u'"%s" is not a directory' % path
return 1
r = Repository('http://')
r.name = name
r.maintainer = self.ask('Enter maintainer of the repository')
r.save(os.path.join(path, r.INDEX))
print u'Repository "%s" created.' % path
def do_build(self, line):
"""
build SOURCE REPOSITORY
Build backends contained in SOURCE to REPOSITORY.
Example:
$ weboob-repos build $HOME/src/weboob/modules /var/www/updates.weboob.org/0.a/
"""
source_path, repo_path = self.parse_command_args(line, 2, 2)
index_file = os.path.join(repo_path, Repository.INDEX)
r = Repository('http://')
try:
with open(index_file, 'r') as fp:
r.parse_index(fp)
except IOError as e:
print >>self.stderr, 'Unable to open repository: %s' % e
print >>self.stderr, 'Use the "create" command before.'
return 1
r.build_index(source_path, index_file)
if r.signed:
sigfiles = [r.KEYRING, Repository.INDEX]
gpg = self._find_gpg()
if not gpg:
raise Exception('Unable to find the gpg executable.')
krname = os.path.join(repo_path, r.KEYRING)
if os.path.exists(krname):
kr_mtime = int(datetime.fromtimestamp(os.path.getmtime(krname)).strftime('%Y%m%d%H%M'))
if not os.path.exists(krname) or kr_mtime < r.key_update:
print 'Generate keyring'
# Remove all existing keys
if os.path.exists(krname):
os.remove(krname)
# Add all valid keys
for keyfile in os.listdir(os.path.join(source_path, r.KEYDIR)):
print 'Adding key %s' % keyfile
keypath = os.path.join(source_path, r.KEYDIR, keyfile)
subprocess.check_call([
gpg,
'--no-options',
'--quiet',
'--no-default-keyring',
'--keyring', os.path.realpath(krname),
'--import', os.path.realpath(keypath)])
# Does not make much sense in our case
if os.path.exists(krname + '~'):
os.remove(krname + '~')
if not os.path.exists(krname):
raise Exception('No valid key file found.')
kr_mtime = mktime(strptime(str(r.key_update), '%Y%m%d%H%M'))
os.chmod(krname, 0o644)
os.utime(krname, (kr_mtime, kr_mtime))
else:
print 'Keyring is up to date'
for name, module in r.modules.iteritems():
tarname = os.path.join(repo_path, '%s.tar.gz' % name)
if r.signed:
sigfiles.append(os.path.basename(tarname))
module_path = os.path.join(source_path, name)
if os.path.exists(tarname):
tar_mtime = int(datetime.fromtimestamp(os.path.getmtime(tarname)).strftime('%Y%m%d%H%M'))
if tar_mtime >= module.version:
continue
print 'Create archive for %s' % name
with closing(tarfile.open(tarname, 'w:gz')) as tar:
tar.add(module_path, arcname=name, exclude=self._archive_excludes)
tar_mtime = mktime(strptime(str(module.version), '%Y%m%d%H%M'))
os.utime(tarname, (tar_mtime, tar_mtime))
# Copy icon.
icon_path = os.path.join(module_path, 'favicon.png')
if os.path.exists(icon_path):
shutil.copy(icon_path, os.path.join(repo_path, '%s.png' % name))
if r.signed:
# Find out which keys are allowed to sign
fingerprints = [gpgline.strip(':').split(':')[-1]
for gpgline
in subprocess.Popen([
gpg,
'--no-options',
'--with-fingerprint', '--with-colons',
'--list-public-keys',
'--no-default-keyring',
'--keyring', os.path.realpath(krname)],
stdout=subprocess.PIPE).communicate()[0].splitlines()
if gpgline.startswith('fpr:')]
# Find out the first secret key we have that is allowed to sign
secret_fingerprint = None
for fingerprint in fingerprints:
proc = subprocess.Popen([
gpg,
'--no-options',
'--list-secret-keys', fingerprint],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
proc.communicate()
# if failed
if proc.returncode:
continue
secret_fingerprint = fingerprint
if secret_fingerprint is None:
raise Exception('No suitable secret key found')
# Check if all files have an up to date signature
for filename in sigfiles:
filepath = os.path.realpath(os.path.join(repo_path, filename))
sigpath = filepath + '.sig'
file_mtime = int(os.path.getmtime(filepath))
if os.path.exists(sigpath):
sig_mtime = int(os.path.getmtime(sigpath))
if not os.path.exists(sigpath) or sig_mtime < file_mtime:
print 'Signing %s' % filename
if os.path.exists(sigpath):
os.remove(sigpath)
subprocess.check_call([
gpg,
'--no-options',
'--quiet',
'--local-user', secret_fingerprint,
'--detach-sign',
'--output', sigpath,
'--sign', filepath])
os.utime(sigpath, (file_mtime, file_mtime))
print 'Signatures are up to date'
@staticmethod
def _find_gpg():
if os.getenv('GPG_EXECUTABLE'):
return os.getenv('GPG_EXECUTABLE')
paths = os.getenv('PATH', os.defpath).split(os.pathsep)
for path in paths:
for ex in ('gpg2', 'gpg'):
fpath = os.path.join(path, ex)
if os.path.exists(fpath) and os.access(fpath, os.X_OK):
return fpath
def _archive_excludes(self, filename):
# Skip *.pyc files in tarballs.
if filename.endswith('.pyc'):
return True
# Don't include *.png files in tarball
if filename.endswith('.png'):
return True
return False
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboorrents/ 0000775 0000000 0000000 00000000000 12411220504 0026700 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboorrents/__init__.py 0000664 0000000 0000000 00000001440 12411220504 0031010 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .weboorrents import Weboorrents
__all__ = ['Weboorrents']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/weboorrents/weboorrents.py 0000664 0000000 0000000 00000014610 12411220504 0031625 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.torrent import CapTorrent, MagnetOnly
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.core import CallErrors
from weboob.capabilities.base import NotAvailable, empty
__all__ = ['Weboorrents']
def sizeof_fmt(num):
for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
if num < 1024.0:
return "%-4.1f%s" % (num, x)
num /= 1024.0
class TorrentInfoFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'name', 'size', 'seeders', 'leechers', 'url', 'files', 'description')
def format_obj(self, obj, alias):
result = u'%s%s%s\n' % (self.BOLD, obj.name, self.NC)
result += 'ID: %s\n' % obj.fullid
if obj.size != NotAvailable:
result += 'Size: %s\n' % sizeof_fmt(obj.size)
result += 'Seeders: %s\n' % obj.seeders
result += 'Leechers: %s\n' % obj.leechers
result += 'URL: %s\n' % obj.url
if hasattr(obj, 'magnet') and obj.magnet:
result += 'Magnet URL: %s\n' % obj.magnet
if obj.files:
result += '\n%sFiles%s\n' % (self.BOLD, self.NC)
for f in obj.files:
result += ' * %s\n' % f
result += '\n%sDescription%s\n' % (self.BOLD, self.NC)
result += '%s' % obj.description
return result
class TorrentListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'size', 'seeders', 'leechers')
def get_title(self, obj):
return obj.name
NB2COLOR = ((0, 'red', None),
(1, 'blue', None),
(5, 'green', None),
(10, 'green', 'bold'),
)
def _get_color(self, nb):
if empty(nb):
return self.colored('N/A', 'red')
for threshold, _color, _attr in self.NB2COLOR:
if nb >= threshold:
color = _color
attr = _attr
return self.colored('%3d' % nb, color, attr)
def get_description(self, obj):
size = self.colored('%10s' % sizeof_fmt(obj.size), 'magenta')
return '%s (Seed: %s / Leech: %s)' % (size,
self._get_color(obj.seeders),
self._get_color(obj.leechers))
class Weboorrents(ReplApplication):
APPNAME = 'weboorrents'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2012 Romain Bignon'
DESCRIPTION = "Console application allowing to search for torrents on various trackers " \
"and download .torrent files."
SHORT_DESCRIPTION = "search and download torrents"
CAPS = CapTorrent
EXTRA_FORMATTERS = {'torrent_list': TorrentListFormatter,
'torrent_info': TorrentInfoFormatter,
}
COMMANDS_FORMATTERS = {'search': 'torrent_list',
'info': 'torrent_info',
}
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, id):
"""
info ID
Get information about a torrent.
"""
torrent = self.get_object(id, 'get_torrent', ('description', 'files'))
if not torrent:
print >>self.stderr, 'Torrent not found: %s' % id
return 3
self.start_format()
self.format(torrent)
def complete_getfile(self, text, line, *ignored):
args = line.split(' ', 2)
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_getfile(self, line):
"""
getfile ID [FILENAME]
Get the .torrent file.
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
"""
id, dest = self.parse_command_args(line, 2, 1)
torrent = self.get_object(id, 'get_torrent', ('description', 'files'))
if not torrent:
print >>self.stderr, 'Torrent not found: %s' % id
return 3
dest = self.obj_to_filename(torrent, dest, '{id}-{name}.torrent')
try:
for backend, buf in self.do('get_torrent_file', torrent.id, backends=torrent.backend):
if buf:
if dest == '-':
print buf
else:
try:
with open(dest, 'w') as f:
f.write(buf)
except IOError as e:
print >>self.stderr, 'Unable to write .torrent in "%s": %s' % (dest, e)
return 1
return
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, MagnetOnly):
print >>self.stderr, u'Error(%s): No direct URL available, ' \
u'please provide this magnet URL ' \
u'to your client:\n%s' % (backend, error.magnet)
return 4
else:
self.bcall_error_handler(backend, error, backtrace)
print >>self.stderr, 'Torrent "%s" not found' % id
return 3
@defaultcount(10)
def do_search(self, pattern):
"""
search [PATTERN]
Search torrents.
"""
self.change_path([u'search'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for backend, torrent in self.do('iter_torrents', pattern=pattern):
self.cached_format(torrent)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/wetboobs/ 0000775 0000000 0000000 00000000000 12411220504 0026153 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/wetboobs/__init__.py 0000664 0000000 0000000 00000001427 12411220504 0030270 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .wetboobs import WetBoobs
__all__ = ['WetBoobs']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/applications/wetboobs/wetboobs.py 0000664 0000000 0000000 00000011363 12411220504 0030355 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.weather import CapWeather
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
__all__ = ['WetBoobs']
class ForecastsFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'low', 'high')
temperature_display = staticmethod(lambda t: u'%s' % t.value)
def format_obj(self, obj, alias):
result = u'%s* %-15s%s (%s - %s)' % (self.BOLD, '%s:' % obj.date, self.NC, self.temperature_display(obj.low), self.temperature_display(obj.high))
if hasattr(obj, 'text') and obj.text:
result += ' %s' % obj.text
return result
class CurrentFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'temp')
temperature_display = staticmethod(lambda t: u'%s' % t.value)
def format_obj(self, obj, alias):
result = u'%s%s%s: %s' % (self.BOLD, obj.date, self.NC, self.temperature_display(obj.temp))
if hasattr(obj, 'text') and obj.text:
result += u' - %s' % obj.text
return result
class CitiesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name')
def get_title(self, obj):
return obj.name
class WetBoobs(ReplApplication):
APPNAME = 'wetboobs'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2010-2014 Romain Bignon'
DESCRIPTION = "Console application allowing to display weather and forecasts in your city."
SHORT_DESCRIPTION = "display weather and forecasts"
CAPS = CapWeather
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {'cities': CitiesFormatter,
'current': CurrentFormatter,
'forecasts': ForecastsFormatter,
}
COMMANDS_FORMATTERS = {'cities': 'cities',
'current': 'current',
'forecasts': 'forecasts',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
@defaultcount(10)
def do_cities(self, pattern):
"""
cities PATTERN
Search cities.
"""
self.change_path(['cities'])
self.start_format()
for backend, city in self.do('iter_city_search', pattern, caps=CapWeather):
self.cached_format(city)
def complete_current(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_current(self, line):
"""
current CITY_ID
Get current weather for specified city. Use the 'cities' command to find them.
"""
city, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(city)
tr = self.config.get('settings', 'temperature_display', default='C')
if tr == 'C':
self.formatter.temperature_display = lambda t: t.ascelsius()
elif tr == 'F':
self.formatter.temperature_display = lambda t: t.asfahrenheit()
self.start_format()
for backend, current in self.do('get_current', _id, backends=backend_name, caps=CapWeather):
if current:
self.format(current)
def complete_forecasts(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_forecasts(self, line):
"""
forecasts CITY_ID
Get forecasts for specified city. Use the 'cities' command to find them.
"""
city, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(city)
tr = self.config.get('settings', 'temperature_display', default='C')
if tr == 'C':
self.formatter.temperature_display = lambda t: t.ascelsius()
elif tr == 'F':
self.formatter.temperature_display = lambda t: t.asfahrenheit()
self.start_format()
for backend, forecast in self.do('iter_forecast', _id, backends=backend_name, caps=CapWeather):
self.format(forecast)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/ 0000775 0000000 0000000 00000000000 12411220504 0024272 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/__init__.py 0000664 0000000 0000000 00000000262 12411220504 0026403 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from .base import UserError, NotLoaded, NotAvailable, BaseObject, CapBase
__all__ = ['UserError', 'NotLoaded', 'NotAvailable', 'BaseObject', 'CapBase']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/account.py 0000664 0000000 0000000 00000006032 12411220504 0026301 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField, Field, UserError
__all__ = ['AccountRegisterError', 'Account', 'StatusField', 'CapAccount']
class AccountRegisterError(UserError):
"""
Raised when there is an error during registration.
"""
class Account(BaseObject):
"""
Describe an account and its properties.
"""
login = StringField('Login')
password = StringField('Password')
properties = Field('List of key/value properties', dict)
def __init__(self, id=None):
BaseObject.__init__(self, id)
class StatusField(object):
"""
Field of an account status.
"""
FIELD_TEXT = 0x001 # the value is a long text
FIELD_HTML = 0x002 # the value is HTML formated
def __init__(self, key, label, value, flags=0):
self.key = key
self.label = label
self.value = value
self.flags = flags
class CapAccount(CapBase):
"""
Capability for websites when you can create and manage accounts.
:var ACCOUNT_REGISTER_PROPERTIES: This class constant may be a list of
:class:`weboob.tools.value.Value` objects.
If the value remains None, weboob considers
that :func:`register_account` isn't supported.
"""
ACCOUNT_REGISTER_PROPERTIES = None
@staticmethod
def register_account(account):
"""
Register an account on website
This is a static method, it would be called even if the backend is
instancied.
:param account: describe the account to create
:type account: :class:`Account`
:raises: :class:`AccountRegisterError`
"""
raise NotImplementedError()
def confirm_account(self, mail):
"""
From an email go to the confirm link.
"""
raise NotImplementedError()
def get_account(self):
"""
Get the current account.
"""
raise NotImplementedError()
def update_account(self, account):
"""
Update the current account.
"""
raise NotImplementedError()
def get_account_status(self):
"""
Get status of the current account.
:returns: a list of fields
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/audio.py 0000664 0000000 0000000 00000011471 12411220504 0025751 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Pierre Mazière
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import re
from datetime import timedelta
from .image import BaseImage
from .base import Field, StringField, IntField, BaseObject
from .file import CapFile, BaseFile
__all__ = ['BaseAudio', 'CapAudio']
def decode_id(decode_id):
def wrapper(func):
def inner(self, *args, **kwargs):
arg = unicode(args[0])
_id = decode_id(arg)
if _id is None:
return None
new_args = [_id]
new_args.extend(args[1:])
return func(self, *new_args, **kwargs)
return inner
return wrapper
class Album(BaseObject):
"""
Represent an album
"""
title = StringField('album name')
author = StringField('artist name')
year = IntField('release year')
thumbnail = Field('Image associated to the album', BaseImage)
tracks_list = Field('list of tracks', list)
def __init__(self, _id):
BaseObject.__init__(self, unicode("album.%s" % _id))
@classmethod
def decode_id(cls, _id):
if _id:
m = re.match('^(album)\.(.*)', _id)
if m:
return m.group(2)
return _id
class Playlist(BaseObject):
"""
Represent a playlist
"""
title = StringField('playlist name')
tracks_list = Field('list of tracks', list)
def __init__(self, _id):
BaseObject.__init__(self, unicode("playlist.%s" % _id))
@classmethod
def decode_id(cls, _id):
if _id:
m = re.match('^(playlist)\.(.*)', _id)
if m:
return m.group(2)
return _id
class BaseAudio(BaseFile):
"""
Represent an audio file
"""
duration = Field('file duration', int, long, timedelta)
bitrate = Field('file bit rate in Kbps', int)
format = StringField('file format')
thumbnail = Field('Image associated to the file', BaseImage)
def __init__(self, _id):
BaseFile.__init__(self, unicode("audio.%s" % _id))
@classmethod
def decode_id(cls, _id):
if _id:
m = re.match('^(audio)\.(.*)', _id)
if m:
return m.group(2)
return _id
class CapAudio(CapFile):
"""
Audio file provider
"""
@classmethod
def get_object_method(cls, _id):
m = re.match('^(\w+)\.(.*)', _id)
if m:
if m.group(1) == 'album':
return 'get_album'
elif m.group(1) == 'playlist':
return 'get_playlist'
else:
return 'get_audio'
def search_audio(self, pattern, sortby=CapFile.SEARCH_RELEVANCE):
"""
search for a audio file
:param pattern: pattern to search on
:type pattern: str
:param sortby: sort by ...(use SEARCH_* constants)
:rtype: iter[:class:`BaseAudio`]
"""
return self.search_file(pattern, sortby)
def search_album(self, pattern, sortby=CapFile.SEARCH_RELEVANCE):
"""
search for an album
:param pattern: pattern to search on
:type pattern: str
:rtype: iter[:class:`Album`]
"""
raise NotImplementedError()
def search_playlist(self, pattern, sortby=CapFile.SEARCH_RELEVANCE):
"""
search for an album
:param pattern: pattern to search on
:type pattern: str
:rtype: iter[:class:`Playlist`]
"""
raise NotImplementedError()
@decode_id(BaseAudio.decode_id)
def get_audio(self, _id):
"""
Get an audio file from an ID.
:param id: audio file ID
:type id: str
:rtype: :class:`BaseAudio`]
"""
return self.get_file(_id)
@decode_id(Playlist.decode_id)
def get_playlist(self, _id):
"""
Get a playlist from an ID.
:param id: playlist ID
:type id: str
:rtype: :class:`Playlist`]
"""
raise NotImplementedError()
@decode_id(Album.decode_id)
def get_album(self, _id):
"""
Get an album from an ID.
:param id: album ID
:type id: str
:rtype: :class:`Album`]
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/audiostream.py 0000664 0000000 0000000 00000003531 12411220504 0027163 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Pierre Mazière
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.capabilities.streaminfo import StreamInfo
from .base import Field
from .file import CapFile
from .audio import CapAudio, BaseAudio
__all__ = ['BaseAudioStream', 'CapAudioStream']
class BaseAudioStream(BaseAudio):
"""
Audio stream object
"""
current = Field('Information related to current broadcast', StreamInfo)
def __unicode__(self):
return u'%s (%s)' % (self.title, self.url)
def __repr__(self):
return self.__unicode__()
class CapAudioStream(CapAudio):
"""
Audio streams provider
"""
def search_audiostreams(self, pattern, sortby=CapFile.SEARCH_RELEVANCE):
"""
Search an audio stream
:param pattern: pattern to search
:type pattern: str
:param sortby: sort by ... (use SEARCH_* constants)
:rtype: iter[:class:`BaseAudioStream`]
"""
return self.search_audio(pattern, sortby)
def get_audiostream(self, _id):
"""
Get an audio stream
:param _id: Audiostream ID
:type id: str
:rtype: :class:`BaseAudioStream`
"""
return self.get_audio(_id)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/bank.py 0000664 0000000 0000000 00000021307 12411220504 0025562 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from datetime import date, datetime
from binascii import crc32
import re
from weboob.tools.compat import basestring, long
from .base import BaseObject, Field, StringField, DecimalField, IntField, UserError, Currency
from .date import DateField
from .collection import CapCollection
__all__ = ['AccountNotFound', 'TransferError', 'Recipient', 'Account', 'Transaction', 'Investment', 'Transfer', 'CapBank']
class AccountNotFound(UserError):
"""
Raised when an account is not found.
"""
def __init__(self, msg='Account not found'):
UserError.__init__(self, msg)
class TransferError(UserError):
"""
A transfer has failed.
"""
class Recipient(BaseObject, Currency):
"""
Recipient of a transfer.
"""
label = StringField('Name')
currency = StringField('Currency', default=None)
def __init__(self):
BaseObject.__init__(self, 0)
@property
def currency_text(self):
return Currency.currency2txt(self.currency)
class Account(Recipient):
"""
Bank account.
It is a child class of :class:`Recipient`, because an account can be
a recipient of a transfer.
"""
TYPE_UNKNOWN = 0
TYPE_CHECKING = 1
"Transaction, everyday transactions"
TYPE_SAVINGS = 2
"Savings/Deposit, can be used for every banking"
TYPE_DEPOSIT = 3
"Term of Fixed Deposit, has time/amount constraints"
TYPE_LOAN = 4
"Loan account"
TYPE_MARKET = 5
"Stock market or other variable investments"
TYPE_JOINT = 6
"Joint account"
TYPE_CARD = 7
"Card account"
type = IntField('Type of account', default=TYPE_UNKNOWN)
balance = DecimalField('Balance on this bank account')
coming = DecimalField('Coming balance')
def __repr__(self):
return u"" % (self.id, self.label)
class Transaction(BaseObject):
"""
Bank transaction.
"""
TYPE_UNKNOWN = 0
TYPE_TRANSFER = 1
TYPE_ORDER = 2
TYPE_CHECK = 3
TYPE_DEPOSIT = 4
TYPE_PAYBACK = 5
TYPE_WITHDRAWAL = 6
TYPE_CARD = 7
TYPE_LOAN_PAYMENT = 8
TYPE_BANK = 9
TYPE_CASH_DEPOSIT = 10
date = DateField('Debit date on the bank statement')
rdate = DateField('Real date, when the payment has been made; usually extracted from the label or from credit card info')
vdate = DateField('Value date, or accounting date; usually for professional accounts')
type = IntField('Type of transaction, use TYPE_* constants', default=TYPE_UNKNOWN)
raw = StringField('Raw label of the transaction')
category = StringField('Category of the transaction')
label = StringField('Pretty label')
amount = DecimalField('Amount of the transaction')
def __repr__(self):
return "" % (self.date, self.label, self.amount)
def unique_id(self, seen=None, account_id=None):
"""
Get an unique ID for the transaction based on date, amount and raw.
:param seen: if given, the method uses this dictionary as a cache to
prevent several transactions with the same values to have the same
unique ID.
:type seen: :class:`dict`
:param account_id: if given, add the account ID in data used to create
the unique ID. Can be useful if you want your ID to be unique across
several accounts.
:type account_id: :class:`str`
:returns: an unique ID encoded in 8 length hexadecimal string (for example ``'a64e1bc9'``)
:rtype: :class:`str`
"""
crc = crc32(str(self.date))
crc = crc32(str(self.amount), crc)
crc = crc32(re.sub('[ ]+', ' ', self.raw.encode("utf-8")), crc)
if account_id is not None:
crc = crc32(str(account_id), crc)
if seen is not None:
while crc in seen:
crc = crc32("*", crc)
seen.add(crc)
return "%08x" % (crc & 0xffffffff)
class Investment(BaseObject):
"""
Investment in a financial market.
"""
label = StringField('Label of stocks')
code = StringField('Short code identifier of the stock')
quantity = IntField('Quantity of stocks')
unitprice = DecimalField('Buy price of one stock')
unitvalue = DecimalField('Current value of one stock')
valuation = DecimalField('Total current valuation of the Investment')
diff = DecimalField('Difference between the buy cost and the current valuation')
class Transfer(BaseObject):
"""
Transfer from an account to a recipient.
"""
amount = DecimalField('Amount to transfer')
date = Field('Date of transfer', basestring, date, datetime)
origin = Field('Origin of transfer', int, long, basestring)
recipient = Field('Recipient', int, long, basestring)
reason = StringField('Reason')
class CapBank(CapCollection):
"""
Capability of bank websites to see accounts and transactions.
"""
def iter_resources(self, objs, split_path):
"""
Iter resources.
Default implementation of this method is to return on top-level
all accounts (by calling :func:`iter_accounts`).
:param objs: type of objects to get
:type objs: tuple[:class:`BaseObject`]
:param split_path: path to discover
:type split_path: :class:`list`
:rtype: iter[:class:`BaseObject`]
"""
if Account in objs:
self._restrict_level(split_path)
return self.iter_accounts()
def iter_accounts(self):
"""
Iter accounts.
:rtype: iter[:class:`Account`]
"""
raise NotImplementedError()
def get_account(self, id):
"""
Get an account from its ID.
:param id: ID of the account
:type id: :class:`str`
:rtype: :class:`Account`
:raises: :class:`AccountNotFound`
"""
raise NotImplementedError()
def iter_history(self, account):
"""
Iter history of transactions on a specific account.
:param account: account to get history
:type account: :class:`Account`
:rtype: iter[:class:`Transaction`]
:raises: :class:`AccountNotFound`
"""
raise NotImplementedError()
def iter_coming(self, account):
"""
Iter coming transactions on a specific account.
:param account: account to get coming transactions
:type account: :class:`Account`
:rtype: iter[:class:`Transaction`]
:raises: :class:`AccountNotFound`
"""
raise NotImplementedError()
def iter_transfer_recipients(self, account):
"""
Iter recipients availables for a transfer from a specific account.
:param account: account which initiate the transfer
:type account: :class:`Account`
:rtype: iter[:class:`Recipient`]
:raises: :class:`AccountNotFound`
"""
raise NotImplementedError()
def transfer(self, account, recipient, amount, reason=None):
"""
Make a transfer from an account to a recipient.
:param account: account to take money
:type account: :class:`Account`
:param recipient: account to send money
:type recipient: :class:`Recipient`
:param amount: amount
:type amount: :class:`decimal.Decimal`
:param reason: reason of transfer
:type reason: :class:`unicode`
:rtype: :class:`Transfer`
:raises: :class:`AccountNotFound`, :class:`TransferError`
"""
raise NotImplementedError()
def iter_investment(self, account):
"""
Iter investment of a market account
:param account: account to get investments
:type account: :class:`Account`
:rtype: iter[:class:`Investment`]
:raises: :class:`AccountNotFound`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/base.py 0000664 0000000 0000000 00000032207 12411220504 0025562 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Christophe Benz, Romain Bignon, Julien Hebert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import warnings
import re
from decimal import Decimal
from copy import deepcopy, copy
from weboob.tools.compat import unicode, long
from weboob.tools.misc import to_unicode
from weboob.tools.ordereddict import OrderedDict
__all__ = ['UserError', 'FieldNotFound', 'NotAvailable',
'NotLoaded', 'CapBase', 'Field', 'IntField', 'DecimalField',
'FloatField', 'StringField', 'BytesField',
'empty', 'BaseObject']
def empty(value):
"""
Checks if a value is empty (None, NotLoaded or NotAvailable).
:rtype: :class:`bool`
"""
for cls in (None, NotLoaded, NotAvailable):
if value is cls:
return True
return False
def find_object(mylist, error=None, **kwargs):
"""
Very simple tools to return an object with the matching parameters in
kwargs.
"""
for a in mylist:
found = True
for key, value in kwargs.iteritems():
if getattr(a, key) != value:
found = False
break
if found:
return a
if error is not None:
raise error()
return None
class UserError(Exception):
"""
Exception containing an error message for user.
"""
class FieldNotFound(Exception):
"""
A field isn't found.
:param obj: object
:type obj: :class:`BaseObject`
:param field: field not found
:type field: :class:`Field`
"""
def __init__(self, obj, field):
Exception.__init__(self,
u'Field "%s" not found for object %s' % (field, obj))
class ConversionWarning(UserWarning):
"""
A field's type was changed when setting it.
Ideally, the module should use the right type before setting it.
"""
pass
class AttributeCreationWarning(UserWarning):
"""
A non-field attribute has been created with a name not
prefixed with a _.
"""
class NotAvailableType(object):
"""
NotAvailable is a constant to use on non available fields.
"""
def __str__(self):
return unicode(self).decode('utf-8')
def __copy__(self):
return self
def __deepcopy__(self, memo):
return self
def __repr__(self):
return 'NotAvailable'
def __unicode__(self):
return u'Not available'
def __nonzero__(self):
return False
NotAvailable = NotAvailableType()
class NotLoadedType(object):
"""
NotLoaded is a constant to use on not loaded fields.
When you use :func:`weboob.tools.backend.Module.fillobj` on a object based on :class:`BaseObject`,
it will request all fields with this value.
"""
def __str__(self):
return unicode(self).decode('utf-8')
def __copy__(self):
return self
def __deepcopy__(self, memo):
return self
def __repr__(self):
return u'NotLoaded'
def __unicode__(self):
return u'Not loaded'
def __nonzero__(self):
return False
NotLoaded = NotLoadedType()
class CapBase(object):
"""
This is the base class for all capabilities.
A capability may define abstract methods (which raise :class:`NotImplementedError`)
with an explicit docstring to tell backends how to implement them.
Also, it may define some *objects*, using :class:`BaseObject`.
"""
class Field(object):
"""
Field of a :class:`BaseObject` class.
:param doc: docstring of the field
:type doc: :class:`str`
:param args: list of types accepted
:param default: default value of this field. If not specified, :class:`NotLoaded` is used.
"""
_creation_counter = 0
def __init__(self, doc, *args, **kwargs):
self.types = ()
self.value = kwargs.get('default', NotLoaded)
self.doc = doc
for arg in args:
if isinstance(arg, type):
self.types += (arg,)
else:
raise TypeError('Arguments must be types')
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
def convert(self, value):
"""
Convert value to the wanted one.
"""
return value
class IntField(Field):
"""
A field which accepts only :class:`int` and :class:`long` types.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, int, long, **kwargs)
def convert(self, value):
return int(value)
class DecimalField(Field):
"""
A field which accepts only :class:`decimal` type.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, Decimal, **kwargs)
def convert(self, value):
if isinstance(value, Decimal):
return value
return Decimal(value)
class FloatField(Field):
"""
A field which accepts only :class:`float` type.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, float, **kwargs)
def convert(self, value):
return float(value)
class StringField(Field):
"""
A field which accepts only :class:`unicode` strings.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, unicode, **kwargs)
def convert(self, value):
return to_unicode(value)
class BytesField(Field):
"""
A field which accepts only :class:`str` strings.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, str, **kwargs)
def convert(self, value):
if isinstance(value, unicode):
value = value.encode('utf-8')
return str(value)
class _BaseObjectMeta(type):
def __new__(cls, name, bases, attrs):
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1]._creation_counter)
new_class = super(_BaseObjectMeta, cls).__new__(cls, name, bases, attrs)
if new_class._fields is None:
new_class._fields = OrderedDict()
else:
new_class._fields = deepcopy(new_class._fields)
new_class._fields.update(fields)
if new_class.__doc__ is None:
new_class.__doc__ = ''
for name, field in fields:
doc = '(%s) %s' % (', '.join([':class:`%s`' % v.__name__ for v in field.types]), field.doc)
if field.value is not NotLoaded:
doc += ' (default: %s)' % field.value
new_class.__doc__ += '\n:var %s: %s' % (name, doc)
return new_class
class BaseObject(object):
"""
This is the base class for a capability object.
A capability interface may specify to return several kind of objects, to formalise
retrieved information from websites.
As python is a flexible language where variables are not typed, we use a system to
force backends to set wanted values on all fields. To do that, we use the :class:`Field`
class and all derived ones.
For example::
class Transfer(BaseObject):
" Transfer from an account to a recipient. "
amount = DecimalField('Amount to transfer')
date = Field('Date of transfer', basestring, date, datetime)
origin = Field('Origin of transfer', int, long, basestring)
recipient = Field('Recipient', int, long, basestring)
The docstring is mandatory.
"""
__metaclass__ = _BaseObjectMeta
id = None
backend = None
_fields = None
def __init__(self, id=u'', backend=None):
self.id = to_unicode(id)
self.backend = backend
self._fields = deepcopy(self._fields)
@property
def fullid(self):
"""
Full ID of the object, in form '**ID@backend**'.
"""
return '%s@%s' % (self.id, self.backend)
def __iscomplete__(self):
"""
Return True if the object is completed.
It is usefull when the object is a field of an other object which is
going to be filled.
The default behavior is to iter on fields (with iter_fields) and if
a field is NotLoaded, return False.
"""
for key, value in self.iter_fields():
if value is NotLoaded:
return False
return True
def copy(self):
obj = copy(self)
obj._fields = copy(self._fields)
return obj
def __deepcopy__(self, memo):
return self.copy()
def set_empty_fields(self, value, excepts=()):
"""
Set the same value on all empty fields.
:param value: value to set on all empty fields
:param excepts: if specified, do not change fields listed
"""
for key, old_value in self.iter_fields():
if empty(old_value) and key not in excepts:
setattr(self, key, value)
def iter_fields(self):
"""
Iterate on the fields keys and values.
Can be overloaded to iterate on other things.
:rtype: iter[(key, value)]
"""
if hasattr(self, 'id') and self.id is not None:
yield 'id', self.id
for name, field in self._fields.iteritems():
yield name, field.value
def __eq__(self, obj):
if isinstance(obj, BaseObject):
return self.backend == obj.backend and self.id == obj.id
else:
return False
def __getattr__(self, name):
if self._fields is not None and name in self._fields:
return self._fields[name].value
else:
raise AttributeError("'%s' object has no attribute '%s'" % (
self.__class__.__name__, name))
def __setattr__(self, name, value):
try:
attr = (self._fields or {})[name]
except KeyError:
if not name in dir(self) and not name.startswith('_'):
warnings.warn('Creating a non-field attribute %s. Please prefix it with _' % name,
AttributeCreationWarning, stacklevel=2)
object.__setattr__(self, name, value)
else:
if not empty(value):
try:
# Try to convert value to the wanted one.
nvalue = attr.convert(value)
# If the value was converted
if nvalue is not value:
warnings.warn('Value %s was converted from %s to %s' %
(name, type(value), type(nvalue)),
ConversionWarning, stacklevel=2)
value = nvalue
except Exception:
# error during conversion, it will probably not
# match the wanted following types, so we'll
# raise ValueError.
pass
if not isinstance(value, attr.types) and not empty(value):
raise ValueError(
'Value for "%s" needs to be of type %r, not %r' % (
name, attr.types, type(value)))
attr.value = value
def __delattr__(self, name):
try:
self._fields.pop(name)
except KeyError:
object.__delattr__(self, name)
def to_dict(self):
def iter_decorate(d):
for key, value in d:
if key == 'id' and self.backend is not None:
value = self.fullid
yield key, value
fields_iterator = self.iter_fields()
return OrderedDict(iter_decorate(fields_iterator))
class Currency(object):
CURRENCIES = {u'EUR': u'€',
u'CHF': u'CHF',
u'USD': u'$',
u'GBP': u'£',
u'LBP': u'ل.ل',
}
EXTRACTOR = re.compile(r'[\d\s,\.\-]', re.UNICODE)
@classmethod
def get_currency(klass, text):
u"""
>>> Currency.get_currency(u'42')
None
>>> Currency.get_currency(u'42 €')
u'EUR'
>>> Currency.get_currency(u'$42')
u'USD'
>>> Currency.get_currency(u'42.000,00€')
u'EUR'
>>> Currency.get_currency(u'$42 USD')
u'USD'
>>> Currency.get_currency(u'%42 USD')
u'USD'
>>> Currency.get_currency(u'US1D')
None
"""
curtexts = klass.EXTRACTOR.sub(' ', text.upper()).split()
for curtext in curtexts:
for currency, symbol in klass.CURRENCIES.iteritems():
if curtext in (currency, symbol) or symbol in curtext:
return currency
return None
@classmethod
def currency2txt(klass, currency):
return klass.CURRENCIES.get(currency, u'')
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/bill.py 0000664 0000000 0000000 00000012022 12411220504 0025563 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon, Florent Fourcot
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import BaseObject, StringField, DecimalField, UserError
from .date import DateField
from .collection import CapCollection
__all__ = ['SubscriptionNotFound', 'BillNotFound', 'Detail', 'Bill', 'Subscription', 'CapBill']
class SubscriptionNotFound(UserError):
"""
Raised when a subscription is not found.
"""
def __init__(self, msg='Subscription not found'):
UserError.__init__(self, msg)
class BillNotFound(UserError):
"""
Raised when a bill is not found.
"""
def __init__(self, msg='Bill not found'):
UserError.__init__(self, msg)
class Detail(BaseObject):
"""
Detail of a subscription
"""
label = StringField('label of the detail line')
infos = StringField('information')
datetime = DateField('date information')
price = DecimalField('Total price, taxes included')
vat = DecimalField('Value added Tax')
currency = StringField('Currency', default=None)
quantity = DecimalField('Number of units consumed')
unit = StringField('Unit of the consumption')
def __init__(self):
BaseObject.__init__(self, 0)
class Bill(BaseObject):
"""
Bill.
"""
date = DateField('The day the bill has been sent to the subscriber')
format = StringField('file format of the bill')
label = StringField('label of bill')
idparent = StringField('id of the parent subscription')
price = DecimalField('Price to pay')
currency = StringField('Currency', default=None)
deadline = DateField('The latest day to pay')
startdate = DateField('The first day the bill applies to')
finishdate = DateField('The last day the bill applies to')
def __init__(self):
BaseObject.__init__(self, 0)
class Subscription(BaseObject):
"""
Subscription to a service.
"""
label = StringField('label of subscription')
subscriber = StringField('whe has subscribed')
validity = DateField('End validity date of the subscription')
renewdate = DateField('Reset date of consumption')
class CapBill(CapCollection):
def iter_resources(self, objs, split_path):
"""
Iter resources. Will return :func:`iter_subscriptions`.
"""
if Subscription in objs:
self._restrict_level(split_path)
return self.iter_subscription()
def iter_subscription(self):
"""
Iter subscriptions.
:rtype: iter[:class:`Subscription`]
"""
raise NotImplementedError()
def get_subscription(self, _id):
"""
Get a subscription.
:param _id: ID of subscription
:rtype: :class:`Subscription`
:raises: :class:`SubscriptionNotFound`
"""
raise NotImplementedError()
def iter_bills_history(self, subscription):
"""
Iter history of a subscription.
:param subscription: subscription to get history
:type subscription: :class:`Subscription`
:rtype: iter[:class:`Detail`]
"""
raise NotImplementedError()
def get_bill(self, id):
"""
Get a bill.
:param id: ID of bill
:rtype: :class:`Bill`
:raises: :class:`BillNotFound`
"""
raise NotImplementedError()
def download_bill(self, id):
"""
Download a bill.
:param id: ID of bill
:rtype: str
:raises: :class:`BillNotFound`
"""
raise NotImplementedError()
def iter_bills(self, subscription):
"""
Iter bills.
:param subscription: subscription to get bills
:type subscription: :class:`Subscription`
:rtype: iter[:class:`Bill`]
"""
raise NotImplementedError()
def get_details(self, subscription):
"""
Get details of a subscription.
:param subscription: subscription to get details
:type subscription: :class:`Subscription`
:rtype: iter[:class:`Detail`]
"""
raise NotImplementedError()
def get_balance(self, subscription):
"""
Get the balance of a subscription.
:param subscription: subscription to get balance
:type subscription: :class:`Subscription`
:rtype: class:`Detail`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/bugtracker.py 0000664 0000000 0000000 00000020652 12411220504 0027002 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, Field, StringField,\
IntField, UserError
from .date import DateField, DeltaField
__all__ = ['IssueError', 'Project', 'User', 'Version', 'Status', 'Attachment',
'Change', 'Update', 'Issue', 'Query', 'CapBugTracker']
class IssueError(UserError):
"""
Raised when there is an error with an issue.
"""
class Project(BaseObject):
"""
Represents a project.
"""
name = StringField('Name of the project')
members = Field('Members of projects', list)
versions = Field('List of versions available for this project', list)
trackers = Field('All trackers', list)
categories = Field('All categories', list)
statuses = Field('Available statuses for issues', list)
priorities = Field('Available priorities for issues', list)
def __init__(self, id, name):
BaseObject.__init__(self, id)
self.name = unicode(name)
def __repr__(self):
return '' % self.name
def find_user(self, id, name):
"""
Find a user from its ID.
If not found, create a :class:`User` with the specified name.
:param id: ID of user
:type id: str
:param name: Name of user
:type name: str
:rtype: :class:`User`
"""
for user in self.members:
if user.id == id:
return user
if name is None:
return None
return User(id, name)
def find_version(self, id, name):
"""
Find a version from an ID.
If not found, create a :class:`Version` with the specified name.
:param id: ID of version
:type id: str
:param name: Name of version
:type name: str
:rtype: :class:`Version`
"""
for version in self.versions:
if version.id == id:
return version
if name is None:
return None
return Version(id, name)
def find_status(self, name):
"""
Find a status from a name.
:param name: Name of status
:type name: str
:rtype: :class:`Status`
"""
for status in self.statuses:
if status.name == name:
return status
if name is None:
return None
return None
class User(BaseObject):
"""
User.
"""
name = StringField('Name of user')
def __init__(self, id, name):
BaseObject.__init__(self, id)
self.name = unicode(name)
def __repr__(self):
return '' % self.name
class Version(BaseObject):
"""
Version of a project.
"""
name = StringField('Name of version')
def __init__(self, id, name):
BaseObject.__init__(self, id)
self.name = unicode(name)
def __repr__(self):
return '' % self.name
class Status(BaseObject):
"""
Status of an issue.
**VALUE_** constants are the primary status
types.
"""
(VALUE_NEW,
VALUE_PROGRESS,
VALUE_RESOLVED,
VALUE_REJECTED) = range(4)
name = StringField('Name of status')
value = IntField('Value of status (constants VALUE_*)')
def __init__(self, id, name, value):
BaseObject.__init__(self, id)
self.name = unicode(name)
self.value = value
def __repr__(self):
return '' % self.name
class Attachment(BaseObject):
"""
Attachment of an issue.
"""
filename = StringField('Filename')
url = StringField('Direct URL to attachment')
def __repr__(self):
return '' % self.filename
class Change(BaseObject):
"""
A change of an update.
"""
field = StringField('What field has been changed')
last = StringField('Last value of field')
new = StringField('New value of field')
class Update(BaseObject):
"""
Represents an update of an issue.
"""
author = Field('Author of update', User)
date = DateField('Date of update')
hours = DeltaField('Time activity')
message = StringField('Log message')
attachments = Field('Files attached to update', list, tuple)
changes = Field('List of changes', list, tuple)
def __repr__(self):
return '' % self.id
class Issue(BaseObject):
"""
Represents an issue.
"""
project = Field('Project of this issue', Project)
title = StringField('Title of issue')
body = StringField('Text of issue')
creation = DateField('Date when this issue has been created')
updated = DateField('Date when this issue has been updated for the last time')
start = DateField('Date when this issue starts')
due = DateField('Date when this issue is due for')
attachments = Field('List of attached files', list, tuple)
history = Field('History of updates', list, tuple)
author = Field('Author of this issue', User)
assignee = Field('User assigned to this issue', User)
tracker = StringField('Name of the tracker')
category = StringField('Name of the category')
version = Field('Target version of this issue', Version)
status = Field('Status of this issue', Status)
fields = Field('Custom fields (key,value)', dict)
priority = StringField('Priority of the issue') #XXX
class Query(BaseObject):
"""
Query to find an issue.
"""
project = StringField('Filter on projects')
title = StringField('Filter on titles')
author = StringField('Filter on authors')
assignee = StringField('Filter on assignees')
version = StringField('Filter on versions')
category = StringField('Filter on categories')
status = StringField('Filter on statuses')
def __init__(self):
BaseObject.__init__(self, '')
class CapBugTracker(CapBase):
"""
Bug trackers websites.
"""
def iter_issues(self, query):
"""
Iter issues with optionnal patterns.
:param query: query
:type query: :class:`Query`
:rtype: iter[:class:`Issue`]
"""
raise NotImplementedError()
def get_issue(self, id):
"""
Get an issue from its ID.
:param id: ID of issue
:rtype: :class:`Issue`
"""
raise NotImplementedError()
def create_issue(self, project):
"""
Create an empty issue on the given project.
:param project: project
:type project: :class:`Project`
:returns: the created issue
:rtype: :class:`Issue`
"""
raise NotImplementedError()
def post_issue(self, issue):
"""
Post an issue to create or update it.
:param issue: issue to create or update
:type issue: :class:`Issue`
"""
raise NotImplementedError()
def update_issue(self, issue, update):
"""
Add an update to an issue.
:param issue: issue or id of issue
:type issue: :class:`Issue`
:param update: an Update object
:type update: :class:`Update`
"""
raise NotImplementedError()
def remove_issue(self, issue):
"""
Remove an issue.
:param issue: issue
:type issue: :class:`Issue`
"""
raise NotImplementedError()
def iter_projects(self):
"""
Iter projects.
:rtype: iter[:class:`Project`]
"""
raise NotImplementedError()
def get_project(self, id):
"""
Get a project from its ID.
:rtype: :class:`Project`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/calendar.py 0000664 0000000 0000000 00000014242 12411220504 0026420 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import BaseObject, StringField, IntField, FloatField, Field
from .collection import CapCollection, CollectionNotFound, Collection
from .date import DateField
from datetime import time, datetime
from weboob.tools.date import parse_date
__all__ = ['BaseCalendarEvent', 'CapCalendarEvent']
def enum(**enums):
_values = enums.values()
_items = enums.items()
_index = dict((value, i) for i, value in enumerate(enums.values()))
_types = list((type(value) for value in enums.values()))
enums['values'] = _values
enums['items'] = _items
enums['index'] = _index
enums['types'] = _types
return type('Enum', (), enums)
CATEGORIES = enum(CONCERT=u'Concert', CINE=u'Cinema', THEATRE=u'Theatre', TELE=u'Television', CONF=u'Conference')
#the following elements deal with ICalendar stantdards
#see http://fr.wikipedia.org/wiki/ICalendar#Ev.C3.A9nements_.28VEVENT.29
TRANSP = enum(OPAQUE=u'OPAQUE', TRANSPARENT=u'TRANSPARENT')
STATUS = enum(TENTATIVE=u'TENTATIVE', CONFIRMED=u'CONFIRMED', CANCELLED=u'CANCELLED')
class BaseCalendarEvent(BaseObject):
"""
Represents a calendar event
"""
url = StringField('URL of the event')
start_date = DateField('Start date of the event')
end_date = DateField('End date of the event')
summary = StringField('Title of the event')
city = StringField('Name of the city in witch event will take place')
location = StringField('Location of the event')
category = Field('Category of the event', *CATEGORIES.types)
description = StringField('Description of the event')
price = FloatField('Price of the event')
booked_entries = IntField('Entry number')
max_entries = IntField('Max entry number')
event_planner = StringField('Name of the event planner')
#the following elements deal with ICalendar stantdards
#see http://fr.wikipedia.org/wiki/ICalendar#Ev.C3.A9nements_.28VEVENT.29
sequence = IntField('Number of updates, the first is number 1')
# (TENTATIVE, CONFIRMED, CANCELLED)
status = Field('Status of theevent', *STATUS.types)
# (OPAQUE, TRANSPARENT)
transp = Field('Describes if event is available', *TRANSP.types)
@classmethod
def id2url(cls, _id):
"""Overloaded in child classes provided by backends."""
raise NotImplementedError()
@property
def page_url(self):
"""
Get page URL of the announce.
"""
return self.id2url(self.id)
class Query(BaseObject):
"""
Query to find events
"""
start_date = DateField('Start date of the event')
end_date = DateField('End date of the event')
city = StringField('Name of the city in witch event will take place')
categories = Field('List of categories of the event', list, tuple)
def __init__(self):
BaseObject.__init__(self, '')
self.categories = []
for value in CATEGORIES.values:
self.categories.append(value)
class CapCalendarEvent(CapCollection):
"""
Capability of calendar event type sites
"""
ASSOCIATED_CATEGORIES = 'ALL'
def has_matching_categories(self, query):
if self.ASSOCIATED_CATEGORIES == 'ALL':
return True
for category in query.categories:
if category in self.ASSOCIATED_CATEGORIES:
return True
return False
def search_events(self, query):
"""
Search event
:param query: search query
:type query: :class:`Query`
:rtype: iter[:class:`BaseCalendarEvent`]
"""
raise NotImplementedError()
def list_events(self, date_from, date_to=None):
"""
list coming event.
:param date_from: date of beguinning of the events list
:type date_from: date
:param date_to: date of ending of the events list
:type date_to: date
:rtype: iter[:class:`BaseCalendarEvent`]
"""
raise NotImplementedError()
def get_event(self, _id):
"""
Get an event from an ID.
:param _id: id of the event
:type _id: str
:rtype: :class:`BaseCalendarEvent` or None is fot found.
"""
raise NotImplementedError()
def attends_event(self, event, is_attending=True):
"""
Attends or not to an event
:param event : the event
:type event : BaseCalendarEvent
:param is_attending : is attending to the event or not
:type is_attending : bool
"""
raise NotImplementedError()
def iter_resources(self, objs, split_path):
"""
Iter events by category
"""
if len(split_path) == 0 and self.ASSOCIATED_CATEGORIES != 'ALL':
for category in self.ASSOCIATED_CATEGORIES:
collection = Collection([category], category)
yield collection
elif len(split_path) == 1 and split_path[0] in self.ASSOCIATED_CATEGORIES:
query = Query()
query.categories = split_path
query.start_date = datetime.combine(parse_date('today'), time.min)
query.end_date = parse_date('')
query.city = u''
for event in self.search_events(query):
yield event
def validate_collection(self, objs, collection):
"""
Validate Collection
"""
if collection.path_level == 0:
return
if collection.path_level == 1 and collection.split_path[0] in CATEGORIES.values:
return
raise CollectionNotFound(collection.split_path)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/chat.py 0000664 0000000 0000000 00000004405 12411220504 0025566 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import datetime
from .base import CapBase, BaseObject, StringField, UserError
from .date import DateField
__all__ = ['ChatException', 'ChatMessage', 'CapChat']
class ChatException(UserError):
"""
Exception raised when there is a problem with the chat.
"""
class ChatMessage(BaseObject):
"""
Message on the chat.
"""
id_from = StringField('ID of sender')
id_to = StringField('ID of recipient')
message = StringField('Content of message')
date = DateField('Date when the message has been sent')
def __init__(self, id_from, id_to, message, date=None):
BaseObject.__init__(self, '%s.%s' % (id_from, id_to))
self.id_from = id_from
self.id_to = id_to
self.message = message
self.date = date
if self.date is None:
self.date = datetime.datetime.utcnow()
class CapChat(CapBase):
"""
Websites with a chat system.
"""
def iter_chat_messages(self, _id=None):
"""
Iter messages.
:param _id: optional parameter to only get messages
from a given contact.
:type _id: str
:rtype: iter[:class:`ChatMessage`]
"""
raise NotImplementedError()
def send_chat_message(self, _id, message):
"""
Send a message to a contact.
:param _id: ID of recipient
:type _id: str
:param message: message to send
:type message: str
:raises: :class:`ChatException`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/cinema.py 0000664 0000000 0000000 00000012271 12411220504 0026103 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField, IntField, Field
from .date import DateField
__all__ = ['Movie', 'Person', 'CapCinema']
class Movie(BaseObject):
"""
Movie object.
"""
original_title = StringField('Original title of the movie')
other_titles = Field('Titles in other countries', list)
release_date = DateField('Release date of the movie')
all_release_dates= StringField('Release dates list of the movie')
duration = IntField('Duration of the movie in minutes')
short_description= StringField('Short description of the movie')
genres = Field('Genres of the movie', list)
pitch = StringField('Short story description of the movie')
country = StringField('Origin country of the movie')
note = StringField('Notation of the movie')
roles = Field('Lists of Persons related to the movie indexed by roles', dict)
thumbnail_url = StringField('Url of movie thumbnail')
def __init__(self, id, original_title):
BaseObject.__init__(self, id)
self.original_title = original_title
class Person(BaseObject):
"""
Person object.
"""
name = StringField('Star name of a person')
real_name = StringField('Real name of a person')
birth_date = DateField('Birth date of a person')
death_date = DateField('Death date of a person')
birth_place = StringField('City and country of birth of a person')
gender = StringField('Gender of a person')
nationality = StringField('Nationality of a person')
short_biography = StringField('Short biography of a person')
biography = StringField('Full biography of a person')
short_description= StringField('Short description of a person')
roles = Field('Lists of movies related to the person indexed by roles',dict)
thumbnail_url = StringField('Url of person thumbnail')
def __init__(self, id, name):
BaseObject.__init__(self, id)
self.name = name
class CapCinema(CapBase):
"""
Cinema databases.
"""
def iter_movies(self, pattern):
"""
Search movies and iterate on results.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Movies`]
"""
raise NotImplementedError()
def get_movie(self, _id):
"""
Get a movie object from an ID.
:param _id: ID of movie
:type _id: str
:rtype: :class:`Movie`
"""
raise NotImplementedError()
def get_movie_releases(self, _id, country=None):
"""
Get a list of a movie releases from an ID.
:param _id: ID of movie
:type _id: str
:rtype: :class:`String`
"""
raise NotImplementedError()
def iter_movie_persons(self, _id, role=None):
"""
Get the list of persons who are related to a movie.
:param _id: ID of movie
:type _id: str
:rtype: iter[:class:`Person`]
"""
raise NotImplementedError()
def iter_persons(self, pattern):
"""
Search persons and iterate on results.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`persons`]
"""
raise NotImplementedError()
def get_person(self, _id):
"""
Get a person object from an ID.
:param _id: ID of person
:type _id: str
:rtype: :class:`Person`
"""
raise NotImplementedError()
def iter_person_movies(self, _id, role=None):
"""
Get the list of movies related to a person.
:param _id: ID of person
:type _id: str
:rtype: iter[:class:`Movie`]
"""
raise NotImplementedError()
def iter_person_movies_ids(self, _id):
"""
Get the list of movie ids related to a person.
:param _id: ID of person
:type _id: str
:rtype: iter[str]
"""
raise NotImplementedError()
def iter_movie_persons_ids(self, _id):
"""
Get the list of person ids related to a movie.
:param _id: ID of movie
:type _id: str
:rtype: iter[str]
"""
raise NotImplementedError()
def get_person_biography(self, id):
"""
Get the person full biography.
:param _id: ID of person
:type _id: str
:rtype: str
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/collection.py 0000664 0000000 0000000 00000013105 12411220504 0026777 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Nicolas Duhamel, Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.ordereddict import OrderedDict
from .base import CapBase, BaseObject, UserError, StringField, Field
__all__ = ['CapCollection', 'BaseCollection', 'Collection', 'CollectionNotFound']
class CollectionNotFound(UserError):
def __init__(self, split_path=None):
if split_path is not None:
msg = 'Collection not found: %s' % '/'.join(split_path)
else:
msg = 'Collection not found'
UserError.__init__(self, msg)
class BaseCollection(BaseObject):
"""
Inherit from this if you want to create an object that is *also* a Collection.
However, this probably will not work properly for now.
"""
def __init__(self, split_path):
BaseObject.__init__(self, None)
self.split_path = split_path
@property
def basename(self):
return self.split_path[-1] if self.path_level else None
@property
def parent_path(self):
return self.split_path[0:-1] if self.path_level else None
@property
def path_level(self):
return len(self.split_path)
def to_dict(self):
def iter_decorate(d):
for key, value in d:
if key == 'id' and self.backend is not None:
value = u'%s@%s' % (self.basename, self.backend)
yield key, value
if key == 'split_path':
yield key, '/'.join(value)
fields_iterator = self.iter_fields()
return OrderedDict(iter_decorate(fields_iterator))
class Collection(BaseCollection):
"""
A Collection is a "fake" object returned in results, which shows you can get
more results if you go into its path.
It is a dumb object, it must not contain callbacks to a backend.
Do not inherit from this class if you want to make a regular BaseObject
a Collection, use BaseCollection instead.
"""
title = StringField('Collection title')
split_path = Field('Full collection path', list)
def __init__(self, split_path, title=None):
self.title = title
BaseCollection.__init__(self, split_path)
def __unicode__(self):
if self.title and self.basename:
return u'%s (%s)' % (self.basename, self.title)
elif self.basename:
return u'%s' % self.basename
else:
return u'Unknown collection'
class CapCollection(CapBase):
def iter_resources_flat(self, objs, split_path, clean_only=False):
"""
Call iter_resources() to fetch all resources in the tree.
If clean_only is True, do not explore paths, only remove them.
split_path is used to set the starting path.
"""
for resource in self.iter_resources(objs, split_path):
if isinstance(resource, Collection):
if not clean_only:
for res in self.iter_resources_flat(objs, resource.split_path):
yield res
else:
yield resource
def iter_resources(self, objs, split_path):
"""
split_path is a list, either empty (root path) or with one or many
components.
"""
raise NotImplementedError()
def get_collection(self, objs, split_path):
"""
Get a collection for a given split path.
If the path is invalid (i.e. can't be handled by this module),
it should return None.
"""
collection = Collection(split_path, None)
return self.validate_collection(objs, collection) or collection
def validate_collection(self, objs, collection):
"""
Tests if a collection is valid.
For compatibility reasons, and to provide a default way, it checks if
the collection has at least one object in it. However, it is not very
efficient or exact, and you are encouraged to override this method.
You can replace the collection object entirely by returning a new one.
"""
# Root
if collection.path_level == 0:
return
try:
i = self.iter_resources(objs, collection.split_path)
i.next()
except StopIteration:
raise CollectionNotFound(collection.split_path)
def _restrict_level(self, split_path, lmax=0):
if len(split_path) > lmax:
raise CollectionNotFound(split_path)
def test():
c = Collection([])
assert c.basename is None
assert c.parent_path is None
assert c.path_level == 0
c = Collection([u'lol'])
assert c.basename == u'lol'
assert c.parent_path == []
assert c.path_level == 1
c = Collection([u'lol', u'cat'])
assert c.basename == u'cat'
assert c.parent_path == [u'lol']
assert c.path_level == 2
c = Collection([u'w', u'e', u'e', u'b', u'o', u'o', u'b'])
assert c.basename == u'b'
assert c.parent_path == [u'w', u'e', u'e', u'b', u'o', u'o']
assert c.path_level == 7
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/contact.py 0000664 0000000 0000000 00000015612 12411220504 0026304 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, Field, StringField, BytesField, IntField, \
UserError
from weboob.tools.ordereddict import OrderedDict
__all__ = ['ProfileNode', 'ContactPhoto', 'Contact', 'QueryError', 'Query', 'CapContact']
class ProfileNode(object):
"""
Node of a :class:`Contact` profile.
"""
HEAD = 0x01
SECTION = 0x02
def __init__(self, name, label, value, sufix=None, flags=0):
self.name = name
self.label = label
self.value = value
self.sufix = sufix
self.flags = flags
def __getitem__(self, key):
return self.value[key]
class ContactPhoto(BaseObject):
"""
Photo of a contact.
"""
name = StringField('Name of the photo')
url = StringField('Direct URL to photo')
data = BytesField('Data of photo')
thumbnail_url = StringField('Direct URL to thumbnail')
thumbnail_data = BytesField('Data of thumbnail')
hidden = Field('True if the photo is hidden on website', bool)
def __init__(self, name):
BaseObject.__init__(self, name)
self.name = name
def __iscomplete__(self):
return (self.data and (not self.thumbnail_url or self.thumbnail_data))
def __str__(self):
return self.url
def __repr__(self):
return u'' % (self.id,
len(self.data) if self.data else 0,
len(self.thumbnail_data) if self.thumbnail_data else 0)
class Contact(BaseObject):
"""
A contact.
"""
STATUS_ONLINE = 0x001
STATUS_AWAY = 0x002
STATUS_OFFLINE = 0x004
STATUS_ALL = 0xfff
name = StringField('Name of contact')
status = IntField('Status of contact (STATUS_* constants)')
url = StringField('URL to the profile of contact')
status_msg = StringField('Message of status')
summary = StringField('Description of contact')
photos = Field('List of photos', dict, default=OrderedDict())
profile = Field('Contact profile', dict)
def __init__(self, id, name, status):
BaseObject.__init__(self, id)
self.name = name
self.status = status
def set_photo(self, name, **kwargs):
"""
Set photo of contact.
:param name: name of photo
:type name: str
:param kwargs: See :class:`ContactPhoto` to know what other parameters you can use
"""
if not name in self.photos:
self.photos[name] = ContactPhoto(name)
photo = self.photos[name]
for key, value in kwargs.iteritems():
setattr(photo, key, value)
def get_text(self):
def print_node(node, level=1):
result = u''
if node.flags & node.SECTION:
result += u'\t' * level + node.label + '\n'
for sub in node.value.itervalues():
result += print_node(sub, level + 1)
else:
if isinstance(node.value, (tuple, list)):
value = ', '.join(unicode(v) for v in node.value)
elif isinstance(node.value, float):
value = '%.2f' % node.value
else:
value = node.value
result += u'\t' * level + u'%-20s %s\n' % (node.label + ':', value)
return result
result = u'Nickname: %s\n' % self.name
if self.status & Contact.STATUS_ONLINE:
s = 'online'
elif self.status & Contact.STATUS_OFFLINE:
s = 'offline'
elif self.status & Contact.STATUS_AWAY:
s = 'away'
else:
s = 'unknown'
result += u'Status: %s (%s)\n' % (s, self.status_msg)
result += u'URL: %s\n' % self.url
result += u'Photos:\n'
for name, photo in self.photos.iteritems():
result += u'\t%s%s\n' % (photo, ' (hidden)' if photo.hidden else '')
result += u'\nProfile:\n'
for head in self.profile.itervalues():
result += print_node(head)
result += u'Description:\n'
for s in self.summary.split('\n'):
result += u'\t%s\n' % s
return result
class QueryError(UserError):
"""
Raised when unable to send a query to a contact.
"""
class Query(BaseObject):
"""
Query to send to a contact.
"""
message = StringField('Message received')
def __init__(self, id, message):
BaseObject.__init__(self, id)
self.message = message
class CapContact(CapBase):
def iter_contacts(self, status=Contact.STATUS_ALL, ids=None):
"""
Iter contacts
:param status: get only contacts with the specified status
:type status: Contact.STATUS_*
:param ids: if set, get the specified contacts
:type ids: list[str]
:rtype: iter[:class:`Contact`]
"""
raise NotImplementedError()
def get_contact(self, id):
"""
Get a contact from his id.
The default implementation only calls iter_contacts()
with the proper values, but it might be overloaded
by backends.
:param id: the ID requested
:type id: str
:rtype: :class:`Contact` or None if not found
"""
l = self.iter_contacts(ids=[id])
try:
return l[0]
except IndexError:
return None
def send_query(self, id):
"""
Send a query to a contact
:param id: the ID of contact
:type id: str
:rtype: :class:`Query`
:raises: :class:`QueryError`
"""
raise NotImplementedError()
def get_notes(self, id):
"""
Get personal notes about a contact
:param id: the ID of the contact
:type id: str
:rtype: unicode
"""
raise NotImplementedError
def save_notes(self, id, notes):
"""
Set personal notes about a contact
:param id: the ID of the contact
:type id: str
:returns: the unicode object to save as notes
"""
raise NotImplementedError
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/content.py 0000664 0000000 0000000 00000005217 12411220504 0026323 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField, Field
from .date import DateField
__all__ = ['Content', 'Revision', 'CapContent']
class Content(BaseObject):
"""
Content object.
"""
title = StringField('Title of content')
author = StringField('Original author of content')
content = StringField('Body')
revision = StringField('ID of revision')
class Revision(BaseObject):
"""
Revision of a change on a content.
"""
author = StringField('Author of revision')
comment = StringField('Comment log about revision')
timestamp = DateField('Date of revision')
minor = Field('Is this change minor?', bool)
class CapContent(CapBase):
def get_content(self, id, revision=None):
"""
Get a content from an ID.
:param id: ID of content
:type id: str
:param revision: if given, get the content at this revision
:type revision: :class:`Revision`
:rtype: :class:`Content`
"""
raise NotImplementedError()
def iter_revisions(self, id):
"""
Iter revisions of a content.
:param id: id of content
:type id: str
:rtype: iter[:class:`Revision`]
"""
raise NotImplementedError()
def push_content(self, content, message=None, minor=False):
"""
Push a new revision of a content.
:param content: object to push
:type content: :class:`Content`
:param message: log message to associate to new revision
:type message: str
:param minor: this is a minor revision
:type minor: bool
"""
raise NotImplementedError()
def get_content_preview(self, content):
"""
Get a HTML preview of a content.
:param content: content object
:type content: :class:`Content`
:rtype: str
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/date.py 0000664 0000000 0000000 00000004015 12411220504 0025561 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Christophe Benz, Romain Bignon, Julien Hebert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import datetime
from weboob.tools.date import new_date, new_datetime
from weboob.capabilities.base import Field
__all__ = ['DateField', 'TimeField', 'DeltaField']
class DateField(Field):
"""
A field which accepts only :class:`datetime.date` and :class:`datetime.datetime` types.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, datetime.date, datetime.datetime, **kwargs)
def __setattr__(self, name, value):
if name == 'value':
# Force use of our date and datetime types, to fix bugs in python2
# with strftime on year<1900.
if type(value) is datetime.datetime:
value = new_datetime(value)
if type(value) is datetime.date:
value = new_date(value)
return object.__setattr__(self, name, value)
class TimeField(Field):
"""
A field which accepts only :class:`datetime.time` and :class:`datetime.time` types.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, datetime.time, datetime.datetime, **kwargs)
class DeltaField(Field):
"""
A field which accepts only :class:`datetime.timedelta` type.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, datetime.timedelta, **kwargs)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/dating.py 0000664 0000000 0000000 00000007441 12411220504 0026120 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, Field, StringField, UserError
from .date import DateField
from .contact import Contact
__all__ = ['OptimizationNotFound', 'Optimization', 'Event', 'CapDating']
class OptimizationNotFound(UserError):
"""
Raised when an optimization is not found.
"""
class Optimization(object):
"""
Optimization.
:var CONFIG: Configuration of optim can be made by
:class:`weboob.tools.value.Value` objects
in this dict.
"""
CONFIG = {}
def start(self):
"""
Start optimization.
"""
raise NotImplementedError()
def stop(self):
"""
Stop optimization.
"""
raise NotImplementedError()
def is_running(self):
"""
Know if the optimization is currently running.
:rtype: bool
"""
raise NotImplementedError()
def get_config(self):
"""
Get config of this optimization.
:rtype: dict
"""
return None
def set_config(self, params):
"""
Set config of this optimization.
:param params: parameters
:type params: dict
"""
raise NotImplementedError()
class Event(BaseObject):
"""
A dating event (for example a visite, a query received, etc.)
"""
date = DateField('Date of event')
contact = Field('Contact related to this event', Contact)
type = StringField('Type of event')
message = StringField('Message of the event')
class CapDating(CapBase):
"""
Capability for dating websites.
"""
def init_optimizations(self):
"""
Initialization of optimizations.
"""
raise NotImplementedError()
def add_optimization(self, name, optim):
"""
Add an optimization.
:param name: name of optimization
:type name: str
:param optim: optimization
:type optim: :class:`Optimization`
"""
setattr(self, 'OPTIM_%s' % name, optim)
def iter_optimizations(self):
"""
Iter optimizations.
:rtype: iter[:class:`Optimization`]
"""
for attr_name in dir(self):
if not attr_name.startswith('OPTIM_'):
continue
attr = getattr(self, attr_name)
if attr is None:
continue
yield attr_name[6:], attr
def get_optimization(self, optim):
"""
Get an optimization from a name.
:param optim: name of optimization
:type optim: str
:rtype: :class:`Optimization`
"""
optim = optim.upper()
if not hasattr(self, 'OPTIM_%s' % optim):
raise OptimizationNotFound()
return getattr(self, 'OPTIM_%s' % optim)
def iter_events(self):
"""
Iter events.
:rtype: iter[:class:`Event`]
"""
raise NotImplementedError()
def iter_new_contacts(self):
"""
Iter new contacts.
:rtype: iter[:class:`Contact`]
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/file.py 0000664 0000000 0000000 00000005124 12411220504 0025565 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Pierre Mazière
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, NotAvailable, Field, StringField
from .date import DateField
__all__ = ['BaseFile', 'CapFile']
class BaseFile(BaseObject):
"""
Represent a file.
"""
title = StringField('File title')
url = StringField('File URL')
ext = StringField('File extension')
author = StringField('File author')
description = StringField('File description')
date = DateField('File publication date')
size = Field('File size in bytes',int,long, default=NotAvailable)
rating = Field('Rating', int, long, float, default=NotAvailable)
rating_max = Field('Maximum rating', int, long, float, default=NotAvailable)
def __str__(self):
return self.url or ''
def __repr__(self):
return '<%s title=%r url=%r>' % (type(self).__name__, self.title, self.url)
@classmethod
def id2url(cls, _id):
"""
Overloaded in child classes provided by backends.
"""
raise NotImplementedError()
@property
def page_url(self):
"""
Get file page URL
"""
return self.id2url(self.id)
class CapFile(CapBase):
"""
Provide file download
"""
(SEARCH_RELEVANCE,
SEARCH_RATING,
SEARCH_VIEWS,
SEARCH_DATE) = range(4)
def search_file(self, pattern, sortby=SEARCH_RELEVANCE):
"""
:param pattern: pattern to search on
:type pattern: str
:param sortby: sort by ... (user SEARCH_* constants)
:rtype: iter[:class:`BaseFile`]
"""
raise NotImplementedError()
def get_file(self, _id):
"""
Get a file from an ID
:param _id: the file id. I can be a numeric ID, or a page url
:type _id: str
:rtype: :class:`BaseFile` or None if not found.
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/gallery.py 0000664 0000000 0000000 00000010471 12411220504 0026306 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon, Christophe Benz, Noé Rubinstein
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.capabilities.thumbnail import Thumbnail
from .base import CapBase, BaseObject, NotLoaded, Field, StringField, \
BytesField, IntField, FloatField
from .date import DateField
__all__ = ['BaseGallery', 'BaseImage', 'CapGallery']
class BaseGallery(BaseObject):
"""
Represents a gallery.
This object has to be inherited to specify how to calculate the URL of the gallery from its ID.
"""
title = StringField('Title of gallery')
url = StringField('Direct URL to gallery')
description = StringField('Description of gallery')
cardinality = IntField('Cardinality of gallery')
date = DateField('Date of gallery')
rating = FloatField('Rating of this gallery')
rating_max = FloatField('Max rating available')
thumbnail = Field('Thumbnail', Thumbnail)
def __init__(self, _id, title=NotLoaded, url=NotLoaded, cardinality=NotLoaded, date=NotLoaded,
rating=NotLoaded, rating_max=NotLoaded, thumbnail=NotLoaded, thumbnail_url=None, nsfw=False):
BaseObject.__init__(self, unicode(_id))
self.title = title
self.url = url
self.date = date
self.rating = rating
self.rating_max = rating_max
self.thumbnail = thumbnail
@classmethod
def id2url(cls, _id):
"""Overloaded in child classes provided by backends."""
raise NotImplementedError()
@property
def page_url(self):
"""
Get URL to page of this gallery.
"""
return self.id2url(self.id)
def iter_image(self):
"""
Iter images.
"""
raise NotImplementedError()
class BaseImage(BaseObject):
"""
Base class for images.
"""
index = IntField('Usually page number')
thumbnail = Field('Thumbnail of the image', Thumbnail)
url = StringField('Direct URL to image')
ext = StringField('Extension of image')
data = BytesField('Data of image')
gallery = Field('Reference to the Gallery object', BaseGallery)
def __init__(self, _id, index=None, thumbnail=NotLoaded, url=NotLoaded,
ext=NotLoaded, gallery=None):
BaseObject.__init__(self, unicode(_id))
self.index = index
self.thumbnail = thumbnail
self.url = url
self.ext = ext
self.gallery = gallery
def __str__(self):
return self.url
def __repr__(self):
return '' % self.url
def __iscomplete__(self):
return self.data is not NotLoaded
class CapGallery(CapBase):
"""
This capability represents the ability for a website backend to provide videos.
"""
(SEARCH_RELEVANCE,
SEARCH_RATING,
SEARCH_VIEWS,
SEARCH_DATE) = range(4)
def search_gallery(self, pattern, sortby=SEARCH_RELEVANCE):
"""
Iter results of a search on a pattern.
:param pattern: pattern to search on
:type pattern: str
:param sortby: sort by...
:type sortby: SEARCH_*
:rtype: :class:`BaseGallery`
"""
raise NotImplementedError()
def get_gallery(self, _id):
"""
Get gallery from an ID.
:param _id: the gallery id. It can be a numeric ID, or a page url, or so.
:type _id: str
:rtype: :class:`Gallery`
"""
raise NotImplementedError()
def iter_gallery_images(self, gallery):
"""
Iterate images from a Gallery.
:type gallery: BaseGallery
:rtype: iter(BaseImage)
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/gauge.py 0000664 0000000 0000000 00000006502 12411220504 0025737 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon, Florent Fourcot
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField, FloatField, Field, UserError, empty
from .date import DateField
__all__ = ['Gauge', 'GaugeSensor', 'GaugeMeasure', 'CapGauge', 'SensorNotFound']
class SensorNotFound(UserError):
"""
Not found a sensor
"""
class Gauge(BaseObject):
"""
Gauge class.
"""
name = StringField('Name of gauge')
city = StringField('City of the gauge')
object = StringField('What is evaluate') # For example, name of a river
sensors = Field('List of sensors on the gauge', list)
class GaugeMeasure(BaseObject):
"""
Measure of a gauge sensor.
"""
level = FloatField('Level of measure')
date = DateField('Date of measure')
alarm = StringField('Alarm level')
def __init__(self):
BaseObject.__init__(self)
def __repr__(self):
if empty(self.level):
return "" % self.level
else:
return "" % (self.level, self.alarm, self.date)
class GaugeSensor(BaseObject):
"""
GaugeSensor class.
"""
name = StringField('Name of the sensor')
unit = StringField('Unit of values')
forecast = StringField('Forecast')
address = StringField('Address')
lastvalue = Field('Last value', GaugeMeasure)
history = Field('Value history', list) # lastvalue not included
gaugeid = StringField('Id of the gauge')
def __repr__(self):
return "" % (self.id, self.name)
class CapGauge(CapBase):
def iter_gauges(self, pattern=None):
"""
Iter gauges.
:param pattern: if specified, used to search gauges.
:type pattern: str
:rtype: iter[:class:`Gauge`]
"""
raise NotImplementedError()
def iter_sensors(self, id, pattern=None):
"""
Iter instrument of a gauge.
:param: ID of the gauge
:param pattern: if specified, used to search sensors.
:type pattern: str
:rtype: iter[:class:`GaugeSensor`]
"""
raise NotImplementedError()
def iter_gauge_history(self, id):
"""
Get history of a gauge sensor.
:param id: ID of the gauge sensor
:type id: str
:rtype: iter[:class:`GaugeMeasure`]
"""
raise NotImplementedError()
def get_last_measure(self, id):
"""
Get last measures of a censor.
:param id: ID of the censor.
:type id: str
:rtype: :class:`GaugeMeasure`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/geolocip.py 0000664 0000000 0000000 00000003265 12411220504 0026453 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField, FloatField
__all__ = ['IpLocation', 'CapGeolocIp']
class IpLocation(BaseObject):
"""
Represents the location of an IP address.
"""
city = StringField('City')
region = StringField('Region')
zipcode = StringField('Zip code')
country = StringField('Country')
lt = FloatField('Latitude')
lg = FloatField('Longitude')
host = StringField('Hostname')
tld = StringField('Top Level Domain')
isp = StringField('Internet Service Provider')
def __init__(self, ipaddr):
BaseObject.__init__(self, ipaddr)
class CapGeolocIp(CapBase):
"""
Access information about IP addresses database.
"""
def get_location(self, ipaddr):
"""
Get location of an IP address.
:param ipaddr: IP address
:type ipaddr: str
:rtype: :class:`IpLocation`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/housing.py 0000664 0000000 0000000 00000010244 12411220504 0026321 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, Field, IntField, DecimalField, \
StringField, BytesField
from .date import DateField
__all__ = ['HousingPhoto', 'Housing', 'Query', 'City', 'CapHousing']
class HousingPhoto(BaseObject):
"""
Photo of a housing.
"""
url = StringField('Direct URL to photo')
data = BytesField('Data of photo')
def __init__(self, url):
BaseObject.__init__(self, url.split('/')[-1])
self.url = url
def __iscomplete__(self):
return self.data
def __str__(self):
return self.url
def __repr__(self):
return u'' % (self.id, len(self.data) if self.data else 0)
class Housing(BaseObject):
"""
Content of a housing.
"""
title = StringField('Title of housing')
area = DecimalField('Area of housing, in m2')
cost = DecimalField('Cost of housing')
currency = StringField('Currency of cost')
date = DateField('Date when the housing has been published')
location = StringField('Location of housing')
station = StringField('What metro/bus station next to housing')
text = StringField('Text of the housing')
phone = StringField('Phone number to contact')
photos = Field('List of photos', list)
details = Field('Key/values of details', dict)
class Query(BaseObject):
"""
Query to find housings.
"""
TYPE_RENT = 0
TYPE_SALE = 1
def enum(**enums):
_values = enums.values()
_items = enums.items()
_index = dict((value, i) for i, value in enumerate(enums.values()))
_types = list((type(value) for value in enums.values()))
enums['values'] = _values
enums['items'] = _items
enums['index'] = _index
enums['types'] = _types
return type('Enum', (), enums)
HOUSE_TYPES = enum(APART=u'Apartment',
HOUSE=u'House',
PARKING=u'Parking',
LAND=u'Land',
OTHER=u'Other',
UNKNOWN=u'Unknown')
type = IntField('Type of housing to find (TYPE_* constants)')
cities = Field('List of cities to search in', list, tuple)
area_min = IntField('Minimal area (in m2)')
area_max = IntField('Maximal area (in m2)')
cost_min = IntField('Minimal cost')
cost_max = IntField('Maximal cost')
nb_rooms = IntField('Number of rooms')
house_types = Field('List of house types', list, tuple, default=HOUSE_TYPES.values)
class City(BaseObject):
"""
City.
"""
name = StringField('Name of city')
class CapHousing(CapBase):
"""
Capability of websites to search housings.
"""
def search_housings(self, query):
"""
Search housings.
:param query: search query
:type query: :class:`Query`
:rtype: iter[:class:`Housing`]
"""
raise NotImplementedError()
def get_housing(self, housing):
"""
Get an housing from an ID.
:param housing: ID of the housing
:type housing: str
:rtype: :class:`Housing` or None if not found.
"""
raise NotImplementedError()
def search_city(self, pattern):
"""
Search a city from a pattern.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`City`]
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/image.py 0000664 0000000 0000000 00000005025 12411220504 0025730 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon, Christophe Benz, Noé Rubinstein
# Copyright(C) 2013 Pierre Mazière
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.ordereddict import OrderedDict
from .base import NotLoaded, Field, BytesField
from .file import CapFile, BaseFile
__all__ = ['BaseImage', 'CapImage']
class _BaseImage(BaseFile):
"""
Fake class to allow the inclusion of a BaseImage property within
the real BaseImage class
"""
pass
class BaseImage(_BaseImage):
"""
Represents an image file.
"""
nsfw = Field('Is this Not Safe For Work', bool, default=False)
thumbnail = Field('Thumbnail of the image', _BaseImage)
data = BytesField('Data of image')
def __iscomplete__(self):
return self.data is not NotLoaded
def to_dict(self):
def iter_decorate(d):
for key, value in d:
if key == 'data':
continue
if key == 'id' and self.backend is not None:
value = self.fullid
yield key, value
fields_iterator = self.iter_fields()
return OrderedDict(iter_decorate(fields_iterator))
class CapImage(CapFile):
"""
Image file provider
"""
def search_image(self, pattern, sortby=CapFile.SEARCH_RELEVANCE, nsfw=False):
"""
search for an image file
:param pattern: pattern to search on
:type pattern: str
:param sortby: sort by ...(use SEARCH_* constants)
:param nsfw: include non-suitable for work images if True
:type nsfw: bool
:rtype: iter[:class:`BaseImage`]
"""
return self.search_file(pattern, sortby)
def get_image(self, _id):
"""
Get an image file from an ID.
:param id: image file ID
:type id: str
:rtype: :class:`BaseImage`]
"""
return self.get_file(_id)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/job.py 0000664 0000000 0000000 00000006701 12411220504 0025422 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Bezleputh
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import BaseObject, CapBase, StringField
from .date import DateField
__all__ = ['BaseJobAdvert', 'CapJob']
class BaseJobAdvert(BaseObject):
"""
Represents a job announce.
"""
url = StringField('URL of the announce')
publication_date = DateField('Date when the announce has been published')
society_name = StringField('Name of the society taht published the announce')
place = StringField('Place where the job take place')
job_name = StringField('Name of the job')
title = StringField('Title of the announce')
contract_type = StringField('Type of the contrat : CDI, CDD')
pay = StringField('Amount of the salary')
description = StringField('Description of the job')
formation = StringField('Required formation')
experience = StringField('Required experience')
def __unicode__(self):
message = u'\r\n-- Advert --\r\n'
message += u'id : %s\r\n' % self.id
message += u'url : %s\r\n' % self.url
message += u'publication_date : %s\r\n' % self.publication_date
message += u'society_name : %s\r\n' % self.society_name
message += u'place : %s\r\n' % self.place
message += u'job_name : %s\r\n' % self.job_name
message += u'title : %s\r\n' % self.title
message += u'contract_type : %s\r\n' % self.contract_type
message += u'pay : %s\r\n' % self.pay
message += u'description : %s\r\n' % self.description
message += u'formation : %s\r\n' % self.formation
message += u'experience : %s\r\n' % self.experience
return message
@classmethod
def id2url(cls, _id):
"""Overloaded in child classes provided by backends."""
raise NotImplementedError()
@property
def page_url(self):
"""
Get page URL of the announce.
"""
return self.id2url(self.id)
class CapJob(CapBase):
"""
Capability of job annouce websites.
"""
def search_job(self, pattern=None):
"""
Iter results of a search on a pattern.
:param pattern: pattern to search on
:type pattern: str
:rtype: iter[:class:`BaseJobAdvert`]
"""
raise NotImplementedError()
def advanced_search_job(self):
"""
Iter results of an advanced search
:rtype: iter[:class:`BaseJobAdvert`]
"""
def get_job_advert(self, _id, advert=None):
"""
Get an announce from an ID.
:param _id: id of the advert
:type _id: str
:param advert: the advert
:type advert: BaseJobAdvert
:rtype: :class:`BaseJobAdvert` or None if not found.
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/library.py 0000664 0000000 0000000 00000004250 12411220504 0026311 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Jeremy Monnet
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .collection import CapCollection
from .base import BaseObject, Field, StringField
from .date import DateField
__all__ = ['Book', 'Renew', 'CapBook']
class Book(BaseObject):
"""
Describes a book.
"""
name = StringField('Name of the book')
author = StringField('Author of the book')
location = StringField('Location')
date = DateField('The due date')
late = Field('Are you late?', bool)
class Renew(BaseObject):
"""
A renew message.
"""
message = StringField('Message')
class CapBook(CapCollection):
"""
Library websites.
"""
def iter_resources(self, objs, split_path):
"""
Iter resources. It retuns :func:`iter_books`.
"""
if Book in objs:
self._restrict_level(split_path)
return self.iter_books()
def iter_books(self):
"""
Iter books.
:rtype: iter[:class:`Book`]
"""
raise NotImplementedError()
def get_book(self, _id):
"""
Get a book from an ID.
:param _id: ID of the book
:type _id: str
:rtype: :class:`Book`
"""
raise NotImplementedError()
def get_booked(self, _id):
raise NotImplementedError()
def renew_book(self, _id):
raise NotImplementedError()
def get_rented(self, _id):
raise NotImplementedError()
def search_books(self, _string):
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/lyrics.py 0000664 0000000 0000000 00000003376 12411220504 0026162 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField
__all__ = ['SongLyrics', 'CapLyrics']
class SongLyrics(BaseObject):
"""
Song lyrics object.
"""
title = StringField('Title of the song')
artist = StringField('Artist of the song')
content = StringField('Lyrics of the song')
def __init__(self, id, title):
BaseObject.__init__(self, id)
self.title = title
class CapLyrics(CapBase):
"""
Lyrics websites.
"""
def iter_lyrics(self, criteria, pattern):
"""
Search lyrics by artist or by song
and iterate on results.
:param criteria: 'artist' or 'song'
:type criteria: str
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`SongLyrics`]
"""
raise NotImplementedError()
def get_lyrics(self, _id):
"""
Get a lyrics object from an ID.
:param _id: ID of lyrics
:type _id: str
:rtype: :class:`SongLyrics`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/messages.py 0000664 0000000 0000000 00000014547 12411220504 0026466 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import datetime
import time
from .base import CapBase, BaseObject, NotLoaded, Field, StringField, \
IntField, UserError
from .date import DateField
__all__ = ['Thread', 'Message', 'CapMessages', 'CantSendMessage', 'CapMessagesPost']
# Message and Thread's attributes refer to themselves, and it isn't possible
# in python, so these base classes are used instead.
class _Message(BaseObject):
""" Base message. """
pass
class _Thread(BaseObject):
""" Base Thread. """
pass
class Message(_Message):
"""
Represents a message read or to send.
"""
IS_HTML = 0x001
"The content is HTML formatted"
IS_UNREAD = 0x002
"The message is unread"
IS_RECEIVED = 0x004
"The receiver has read this message"
IS_NOT_RECEIVED = 0x008
"The receiver hass not read this message"
thread = Field('Reference to the thread', _Thread)
title = StringField('Title of message')
sender = StringField('Author of this message')
receivers = Field('Receivers of the message', list)
date = DateField('Date when the message has been sent')
content = StringField('Body of message')
signature = StringField('Optional signature')
parent = Field('Parent message', _Message)
children = Field('Children fields', list)
flags = IntField('Flags (IS_* constants)', default=0)
def __init__(self, thread=NotLoaded,
id=NotLoaded,
title=NotLoaded,
sender=NotLoaded,
receivers=NotLoaded,
date=None,
parent=NotLoaded,
content=NotLoaded,
signature=NotLoaded,
children=NotLoaded,
flags=0):
super(Message, self).__init__(id)
self.thread = thread
self.title = title
self.sender = sender
self.receivers = receivers
self.content = content
self.signature = signature
self.children = children
self.flags = flags
if date is None:
date = datetime.datetime.utcnow()
self.date = date
if isinstance(parent, Message):
self.parent = parent
else:
self.parent = NotLoaded
self._parent_id = parent
@property
def date_int(self):
"""
Date of message as an integer.
"""
return int(time.strftime('%Y%m%d%H%M%S', self.date.timetuple()))
@property
def full_id(self):
"""
Full ID of message (in form '**THREAD_ID.MESSAGE_ID**')
"""
return '%s.%s' % (self.thread.id, self.id)
@property
def full_parent_id(self):
"""
Get the full ID of the parent message (in form '**THREAD_ID.MESSAGE_ID**').
"""
if self.parent:
return self.parent.full_id
elif self._parent_id is None:
return ''
elif self._parent_id is NotLoaded:
return NotLoaded
else:
return '%s.%s' % (self.thread.id, self._parent_id)
def __eq__(self, msg):
if not isinstance(msg, Message):
return False
if self.thread:
return unicode(self.thread.id) == unicode(msg.thread.id) and \
unicode(self.id) == unicode(msg.id)
else:
return unicode(self.id) == unicode(msg.id)
def __repr__(self):
return '' % (
self.full_id, self.title, self.date, self.sender)
class Thread(_Thread):
"""
Thread containing messages.
"""
IS_THREADS = 0x001
IS_DISCUSSION = 0x002
root = Field('Root message', Message)
title = StringField('Title of thread')
date = DateField('Date of thread')
flags = IntField('Flags (IS_* constants)', default=IS_THREADS)
def iter_all_messages(self):
"""
Iter all messages of the thread.
:rtype: iter[:class:`Message`]
"""
if self.root:
yield self.root
for m in self._iter_all_messages(self.root):
yield m
def _iter_all_messages(self, message):
if message.children:
for child in message.children:
yield child
for m in self._iter_all_messages(child):
yield m
class CapMessages(CapBase):
"""
Capability to read messages.
"""
def iter_threads(self):
"""
Iterates on threads, from newers to olders.
:rtype: iter[:class:`Thread`]
"""
raise NotImplementedError()
def get_thread(self, id):
"""
Get a specific thread.
:rtype: :class:`Thread`
"""
raise NotImplementedError()
def iter_unread_messages(self):
"""
Iterates on messages which hasn't been marked as read.
:rtype: iter[:class:`Message`]
"""
raise NotImplementedError()
def set_message_read(self, message):
"""
Set a message as read.
:param message: message read (or ID)
:type message: :class:`Message` or str
"""
raise NotImplementedError()
class CantSendMessage(UserError):
"""
Raised when a message can't be send.
"""
class CapMessagesPost(CapBase):
"""
This capability allow user to send a message.
"""
def post_message(self, message):
"""
Post a message.
:param message: message to send
:type message: :class:`Message`
:raises: :class:`CantSendMessage`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/parcel.py 0000664 0000000 0000000 00000003654 12411220504 0026122 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, Field, StringField, UserError
from .date import DateField
class Event(BaseObject):
date = DateField('Date')
activity = StringField('Activity')
location = StringField('Location')
def __repr__(self):
return u'' % (self.date, self.activity, self.location)
class Parcel(BaseObject):
STATUS_UNKNOWN = 0
STATUS_PLANNED = 1
STATUS_IN_TRANSIT = 2
STATUS_ARRIVED = 3
arrival = DateField('Scheduled arrival date')
status = Field('Status of parcel', int, default=STATUS_UNKNOWN)
info = StringField('Information about parcel status')
history = Field('History', list)
class CapParcel(CapBase):
def get_parcel_tracking(self, id):
"""
Get information abouut a parcel.
:param id: ID of the parcel
:type id: :class:`str`
:rtype: :class:`Parcel`
:raises: :class:`ParcelNotFound`
"""
raise NotImplementedError()
class ParcelNotFound(UserError):
"""
Raised when a parcell is not found.
It can be an user error, or an expired parcel
"""
def __init__(self, msg='Account not found'):
UserError.__init__(self, msg)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/paste.py 0000664 0000000 0000000 00000007107 12411220504 0025765 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2011 Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, NotLoaded, Field, StringField, UserError
__all__ = ['PasteNotFound', 'BasePaste', 'CapPaste']
class PasteNotFound(UserError):
"""
Raised when a paste is not found.
"""
def __init__(self):
return super(PasteNotFound, self).__init__("Paste not found")
class BasePaste(BaseObject):
"""
Represents a pasted text.
"""
title = StringField('Title of paste')
language = StringField('Language of the paste')
contents = StringField('Content of the paste')
public = Field('Is this paste public?', bool)
def __init__(self, _id, title=NotLoaded, language=NotLoaded, contents=NotLoaded,
public=NotLoaded):
BaseObject.__init__(self, unicode(_id))
self.title = title
self.language = language
self.contents = contents
self.public = public
@classmethod
def id2url(cls, _id):
"""Overloaded in child classes provided by backends."""
raise NotImplementedError()
@property
def page_url(self):
"""
Get URL to page of this paste.
"""
return self.id2url(self.id)
class CapPaste(CapBase):
"""
This capability represents the ability for a website backend to store plain text.
"""
def new_paste(self, *args, **kwargs):
"""
Get a new paste object for posting it with the backend.
The parameters should be passed to the object init.
:rtype: :class:`BasePaste`
"""
raise NotImplementedError()
def can_post(self, contents, title=None, public=None, max_age=None):
"""
Checks if the paste can be pasted by this backend.
Some properties are considered required (public/private, max_age) while others
are just bonuses (language).
contents: Can be used to check encodability, maximum length, etc.
title: Can be used to check length, allowed characters. Should not be required.
public: True must be public, False must be private, None do not care.
max_age: Maximum time to live in seconds.
A score of 0 means the backend is not suitable.
A score of 1 means the backend is suitable.
Higher scores means it is more suitable than others with a lower score.
:rtype: int
:returns: score
"""
raise NotImplementedError()
def get_paste(self, url):
"""
Get a Paste from an ID or URL.
:param _id: the paste id. It can be an ID or a page URL.
:type _id: str
:rtype: :class:`BasePaste`
:raises: :class:`PasteNotFound`
"""
raise NotImplementedError()
def post_paste(self, paste, max_age=None):
"""
Post a paste.
:param paste: a Paste object
:type paste: :class:`BasePaste`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/pricecomparison.py 0000664 0000000 0000000 00000004623 12411220504 0030046 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, Field, DecimalField, \
StringField
from .date import DateField
__all__ = ['Shop', 'Price', 'Product', 'CapPriceComparison']
class Product(BaseObject):
"""
A product.
"""
name = StringField('Name of product')
class Shop(BaseObject):
"""
A shop where the price is.
"""
name = StringField('Name of shop')
location = StringField('Location of the shop')
info = StringField('Information about the shop')
class Price(BaseObject):
"""
Price.
"""
date = DateField('Date when this price has been published')
cost = DecimalField('Cost of the product in this shop')
currency = StringField('Currency of the price')
message = StringField('Message related to this price')
shop = Field('Shop information', Shop)
product = Field('Product', Product)
class CapPriceComparison(CapBase):
"""
Capability for price comparison websites.
"""
def search_products(self, pattern=None):
"""
Search products from a pattern.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Product`]
"""
raise NotImplementedError()
def iter_prices(self, product):
"""
Iter prices for a product.
:param product: product to search
:type product: :class:`Product`
:rtype: iter[:class:`Price`]
"""
raise NotImplementedError()
def get_price(self, id):
"""
Get a price from an ID
:param id: ID of price
:type id: str
:rtype: :class:`Price`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/radio.py 0000664 0000000 0000000 00000003265 12411220504 0025750 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Romain Bignon
# Copyright(C) 2013 Pierre Mazière
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, Field, StringField
from weboob.tools.capabilities.streaminfo import StreamInfo
__all__ = ['Radio', 'CapRadio']
class Radio(BaseObject):
"""
Radio object.
"""
title = StringField('Title of radio')
description = StringField('Description of radio')
current = Field('Current emission', StreamInfo)
streams = Field('List of streams', list)
class CapRadio(CapBase):
"""
Capability of radio websites.
"""
def iter_radios_search(self, pattern):
"""
Search a radio.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Radio`]
"""
raise NotImplementedError()
def get_radio(self, id):
"""
Get a radio from an ID.
:param id: ID of radio
:type id: str
:rtype: :class:`Radio`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/recipe.py 0000664 0000000 0000000 00000015032 12411220504 0026114 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField, IntField, Field, empty
import lxml.etree as ET
import base64
import re
import urllib
__all__ = ['Recipe', 'CapRecipe']
class Comment():
def __init__(self, author=None, rate=None, text=None):
self.author = author
self.rate = rate
self.text = text
def __str__(self):
result = u''
if self.author:
result += 'author: %s, ' % self.author
if self.rate:
result += 'note: %s, ' % self.rate
if self.text:
result += 'comment: %s' % self.text
return result
class Recipe(BaseObject):
"""
Recipe object.
"""
title = StringField('Title of the recipe')
author = StringField('Author name of the recipe')
thumbnail_url = StringField('Direct url to recipe thumbnail')
picture_url = StringField('Direct url to recipe picture')
short_description = StringField('Short description of a recipe')
nb_person = Field('The recipe was made for this amount of persons', list)
preparation_time = IntField('Preparation time of the recipe in minutes')
cooking_time = IntField('Cooking time of the recipe in minutes')
ingredients = Field('Ingredient list necessary for the recipe', list)
instructions = StringField('Instruction step list of the recipe')
comments = Field('User comments about the recipe', list)
def __init__(self, id, title):
BaseObject.__init__(self, id)
self.title = title
def toKrecipesXml(self, author=None):
"""
Export recipe to KRecipes XML string
"""
sauthor = u''
if not empty(self.author):
sauthor += '%s@' % self.author
if author is None:
sauthor += 'Cookboob'
else:
sauthor += author
header = u'\n'
initial_xml = '''\
'''
doc = ET.fromstring(initial_xml)
recipe = doc.find('krecipes-recipe')
desc = ET.SubElement(recipe, 'krecipes-description')
title = ET.SubElement(desc, 'title')
title.text = self.title
authors = ET.SubElement(desc, 'author')
authors.text = sauthor
eyield = ET.SubElement(desc, 'yield')
if not empty(self.nb_person):
amount = ET.SubElement(eyield, 'amount')
if len(self.nb_person) == 1:
amount.text = '%s' % self.nb_person[0]
else:
mini = ET.SubElement(amount, 'min')
mini.text = u'%s' % self.nb_person[0]
maxi = ET.SubElement(amount, 'max')
maxi.text = u'%s' % self.nb_person[1]
etype = ET.SubElement(eyield, 'type')
etype.text = 'persons'
if not empty(self.preparation_time):
preptime = ET.SubElement(desc, 'preparation-time')
preptime.text = '%02d:%02d' % (self.preparation_time / 60, self.preparation_time % 60)
if not empty(self.picture_url):
data = urllib.urlopen(self.picture_url).read()
datab64 = base64.encodestring(data)[:-1]
pictures = ET.SubElement(desc, 'pictures')
pic = ET.SubElement(pictures, 'pic', {'format': 'JPEG', 'id': '1'})
pic.text = ET.CDATA(datab64)
if not empty(self.ingredients):
ings = ET.SubElement(recipe, 'krecipes-ingredients')
pat = re.compile('^[0-9]*')
for i in self.ingredients:
sname = u'%s' % i
samount = ''
sunit = ''
first_nums = pat.match(i).group()
if first_nums != '':
samount = first_nums
sname = i.lstrip('0123456789 ')
ing = ET.SubElement(ings, 'ingredient')
am = ET.SubElement(ing, 'amount')
am.text = samount
unit = ET.SubElement(ing, 'unit')
unit.text = sunit
name = ET.SubElement(ing, 'name')
name.text = sname
if not empty(self.instructions):
instructions = ET.SubElement(recipe, 'krecipes-instructions')
instructions.text = self.instructions
if not empty(self.comments):
ratings = ET.SubElement(recipe, 'krecipes-ratings')
for c in self.comments:
rating = ET.SubElement(ratings, 'rating')
if c.author:
rater = ET.SubElement(rating, 'rater')
rater.text = c.author
if c.text:
com = ET.SubElement(rating, 'comment')
com.text = c.text
crits = ET.SubElement(rating, 'criterion')
if c.rate:
crit = ET.SubElement(crits, 'criteria')
critname = ET.SubElement(crit, 'name')
critname.text = 'Overall'
critstars = ET.SubElement(crit, 'stars')
critstars.text = c.rate.split('/')[0]
return header + ET.tostring(doc, encoding='UTF-8', pretty_print=True).decode('utf-8')
class CapRecipe(CapBase):
"""
Recipe providers.
"""
def iter_recipes(self, pattern):
"""
Search recipes and iterate on results.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Recipe`]
"""
raise NotImplementedError()
def get_recipe(self, _id):
"""
Get a recipe object from an ID.
:param _id: ID of recipe
:type _id: str
:rtype: :class:`Recipe`
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/subtitle.py 0000664 0000000 0000000 00000004415 12411220504 0026503 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013 Julien Veyssier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField, IntField, UserError
__all__ = ['Subtitle', 'CapSubtitle']
class LanguageNotSupported(UserError):
"""
Raised when the language is not supported
"""
def __init__(self, msg='language is not supported'):
UserError.__init__(self, msg)
class Subtitle(BaseObject):
"""
Subtitle object.
"""
name = StringField('Name of subtitle')
ext = StringField('Extension of file')
url = StringField('Direct url to subtitle file')
nb_cd = IntField('Number of cd or files')
language = StringField('Language of the subtitle')
description=StringField('Description of corresponding video')
def __init__(self, id, name):
BaseObject.__init__(self, id)
self.name = name
class CapSubtitle(CapBase):
"""
Subtitle providers.
"""
def iter_subtitles(self, pattern):
"""
Search subtitles and iterate on results.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Subtitle`]
"""
raise NotImplementedError()
def get_subtitle(self, _id):
"""
Get a subtitle object from an ID.
:param _id: ID of subtitle
:type _id: str
:rtype: :class:`Subtitle`
"""
raise NotImplementedError()
def get_subtitle_file(self, _id):
"""
Get the content of the subtitle file.
:param _id: ID of subtitle
:type _id: str
:rtype: str
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/torrent.py 0000664 0000000 0000000 00000005124 12411220504 0026343 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon, Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, Field, StringField, FloatField, \
IntField, UserError
from .date import DateField
__all__ = ['MagnetOnly', 'Torrent', 'CapTorrent']
class MagnetOnly(UserError):
"""
Raised when trying to get URL to torrent but only magnet is available.
"""
def __init__(self, magnet):
self.magnet = magnet
UserError.__init__(self, 'Only magnet URL is available')
class Torrent(BaseObject):
"""
Torrent object.
"""
name = StringField('Name of torrent')
size = FloatField('Size of torrent')
date = DateField('Date when torrent has been published')
url = StringField('Direct url to .torrent file')
magnet = StringField('URI of magnet')
seeders = IntField('Number of seeders')
leechers = IntField('Number of leechers')
files = Field('Files in torrent', list)
description = StringField('Description of torrent')
filename = StringField('Name of .torrent file')
def __init__(self, id, name):
BaseObject.__init__(self, id)
self.name = name
class CapTorrent(CapBase):
"""
Torrent trackers.
"""
def iter_torrents(self, pattern):
"""
Search torrents and iterate on results.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Torrent`]
"""
raise NotImplementedError()
def get_torrent(self, _id):
"""
Get a torrent object from an ID.
:param _id: ID of torrent
:type _id: str
:rtype: :class:`Torrent`
"""
raise NotImplementedError()
def get_torrent_file(self, _id):
"""
Get the content of the .torrent file.
:param _id: ID of torrent
:type _id: str
:rtype: str
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/translate.py 0000664 0000000 0000000 00000003727 12411220504 0026652 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012 Lucien Loiseau
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .base import CapBase, BaseObject, StringField, UserError
__all__ = ['TranslationFail', 'LanguageNotSupported', 'CapTranslate']
class LanguageNotSupported(UserError):
"""
Raised when the language is not supported
"""
def __init__(self, msg='language is not supported'):
UserError.__init__(self, msg)
class TranslationFail(UserError):
"""
Raised when no translation matches the given request
"""
def __init__(self, msg='No Translation Available'):
UserError.__init__(self, msg)
class Translation(BaseObject):
"""
Translation.
"""
lang_src = StringField('Source language')
lang_dst = StringField('Destination language')
text = StringField('Translation')
class CapTranslate(CapBase):
"""
Capability of online translation website to translate word or sentence
"""
def translate(self, source_language, destination_language, request):
"""
Perfom a translation.
:param source_language: language in which the request is written
:param destination_language: language to translate the request into
:param request: the sentence to be translated
:rtype: Translation
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/travel.py 0000664 0000000 0000000 00000010237 12411220504 0026144 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon, Julien Hebert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import datetime
from .base import CapBase, BaseObject, StringField, DecimalField, UserError
from .date import TimeField, DeltaField, DateField
__all__ = ['Station', 'Departure', 'RoadStep', 'RoadmapError', 'RoadmapFilters', 'CapTravel']
class Station(BaseObject):
"""
Describes a station.
"""
name = StringField('Name of station')
def __init__(self, id=None, name=None):
BaseObject.__init__(self, id)
self.name = name
def __repr__(self):
return "" % (self.id, self.name)
class Departure(BaseObject):
"""
Describes a departure.
"""
type = StringField('Type of train')
time = TimeField('Departure time')
departure_station = StringField('Departure station')
arrival_station = StringField('Destination of the train')
arrival_time = TimeField('Arrival time')
late = TimeField('Optional late', default=datetime.time())
information = StringField('Informations')
plateform = StringField('Where the train will leave')
price = DecimalField('Price of ticket')
currency = StringField('Currency', default=None)
def __init__(self, id=None, _type=None, _time=None):
BaseObject.__init__(self, id)
self.type = _type
self.time = _time
def __repr__(self):
return u"" % (
self.id, self.type, self.time.strftime('%H:%M'), self.departure_station, self.arrival_station)
class RoadStep(BaseObject):
"""
A step on a roadmap.
"""
line = StringField('When line')
start_time = TimeField('Start of step')
end_time = TimeField('End of step')
departure = StringField('Departure station')
arrival = StringField('Arrival station')
duration = DeltaField('Duration of this step')
class RoadmapError(UserError):
"""
Raised when the roadmap is unable to be calculated.
"""
class RoadmapFilters(BaseObject):
"""
Filters to get a roadmap.
"""
departure_time = DateField('Wanted departure time')
arrival_time = DateField('Wanted arrival time')
def __init__(self):
BaseObject.__init__(self, '')
class CapTravel(CapBase):
"""
Travel websites.
"""
def iter_station_search(self, pattern):
"""
Iterates on search results of stations.
:param pattern: the search pattern
:type pattern: str
:rtype: iter[:class:`Station`]
"""
raise NotImplementedError()
def iter_station_departures(self, station_id, arrival_id=None, date=None):
"""
Iterate on departures.
:param station_id: the station ID
:type station_id: str
:param arrival_id: optionnal arrival station ID
:type arrival_id: str
:param date: optional date
:type date: datetime.datetime
:rtype: iter[:class:`Departure`]
"""
raise NotImplementedError()
def iter_roadmap(self, departure, arrival, filters):
"""
Get a roadmap.
:param departure: name of departure station
:type departure: str
:param arrival: name of arrival station
:type arrival: str
:param filters: filters on search
:type filters: :class:`RoadmapFilters`
:rtype: iter[:class:`RoadStep`]
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/video.py 0000664 0000000 0000000 00000003603 12411220504 0025754 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Romain Bignon, Christophe Benz
# Copyright(C) 2013 Pierre Mazière
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from datetime import timedelta
from .base import Field
from .image import CapImage, BaseImage
__all__ = ['BaseVideo', 'CapVideo']
class BaseVideo(BaseImage):
"""
Represents a video.
This object has to be inherited to specify how to calculate the URL of the video from its ID.
"""
duration = Field('file duration', int, long, timedelta)
class CapVideo(CapImage):
"""
Video file provider.
"""
def search_videos(self, pattern, sortby=CapImage.SEARCH_RELEVANCE, nsfw=False):
"""
search for a video file
:param pattern: pattern to search on
:type pattern: str
:param sortby: sort by... (use SEARCH_* constants)
:param nsfw: include non-suitable for work videos if True
:type nsfw: bool
:rtype: iter[:class:`BaseVideo`]
"""
return self.search_image(pattern, sortby, nsfw)
def get_video(self, _id):
"""
Get a video file from an ID.
:param _id: video file ID
:type _id: str
:rtype: :class:`BaseVideo` or None is fot found.
"""
return self.get_image(_id)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/capabilities/weather.py 0000664 0000000 0000000 00000007616 12411220504 0026315 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from datetime import datetime, date
from .base import CapBase, BaseObject, Field, FloatField, \
StringField, UserError
from .date import DateField
__all__ = ['Forecast', 'Current', 'City', 'CityNotFound', 'Temperature', 'CapWeather']
class Temperature(BaseObject):
value = FloatField('Temperature value')
unit = StringField('Input unit')
def __init__(self, value, unit = u''):
BaseObject.__init__(self, value)
self.value = value
if unit not in [u'C', u'F']:
unit = u''
self.unit = unit
def asfahrenheit(self):
if not self.unit:
return u'%s' % int(round(self.value))
elif self.unit == 'F':
return u'%s°F' % int(round(self.value))
else:
return u'%s°F' % int(round((self.value * 9.0 / 5.0) + 32))
def ascelsius(self):
if not self.unit:
return u'%s' % int(round(self.value))
elif self.unit == 'C':
return u'%s°C' % int(round(self.value))
else:
return u'%s°C' % int(round((self.value - 32.0) * 5.0 / 9.0))
def __repr__(self):
if self.value is not None and self.unit:
return u'%s %s' % (self.value, self.unit)
class Forecast(BaseObject):
"""
Weather forecast.
"""
date = Field('Date for the forecast', datetime, date, basestring)
low = Field('Low temperature', Temperature)
high = Field('High temperature', Temperature)
text = StringField('Comment on forecast')
def __init__(self, date, low, high, text, unit):
BaseObject.__init__(self, unicode(date))
self.date = date
self.low = Temperature(low, unit)
self.high = Temperature(high, unit)
self.text = text
class Current(BaseObject):
"""
Current weather.
"""
date = DateField('Date of measure')
text = StringField('Comment about current weather')
temp = Field('Current temperature', Temperature)
def __init__(self, date, temp, text, unit):
BaseObject.__init__(self, unicode(date))
self.date = date
self.text = text
self.temp = Temperature(temp, unit)
class City(BaseObject):
"""
City where to find weather.
"""
name = StringField('Name of city')
def __init__(self, id, name):
BaseObject.__init__(self, id)
self.name = name
class CityNotFound(UserError):
"""
Raised when a city is not found.
"""
class CapWeather(CapBase):
"""
Capability for weather websites.
"""
def iter_city_search(self, pattern):
"""
Look for a city.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`City`]
"""
raise NotImplementedError()
def get_current(self, city_id):
"""
Get current weather.
:param city_id: ID of the city
:rtype: :class:`Current`
"""
raise NotImplementedError()
def iter_forecast(self, city_id):
"""
Iter forecasts of a city.
:param city_id: ID of the city
:rtype: iter[:class:`Forecast`]
"""
raise NotImplementedError()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/core/ 0000775 0000000 0000000 00000000000 12411220504 0022571 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/core/__init__.py 0000664 0000000 0000000 00000001523 12411220504 0024703 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .bcall import CallErrors
from .ouiboube import Weboob, WebNip
__all__ = ['CallErrors', 'Weboob', 'WebNip']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/core/backendscfg.py 0000664 0000000 0000000 00000012050 12411220504 0025373 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import stat
import os
import sys
try:
from ConfigParser import RawConfigParser, DuplicateSectionError
except ImportError:
from configparser import RawConfigParser, DuplicateSectionError
from logging import warning
__all__ = ['BackendsConfig', 'BackendAlreadyExists']
class BackendAlreadyExists(Exception):
pass
class BackendsConfig(object):
class WrongPermissions(Exception):
pass
def __init__(self, confpath):
self.confpath = confpath
try:
mode = os.stat(confpath).st_mode
except OSError:
if not os.path.isdir(os.path.dirname(confpath)):
os.makedirs(os.path.dirname(confpath))
if sys.platform == 'win32':
fptr = open(confpath, 'w')
fptr.close()
else:
try:
fd = os.open(confpath, os.O_WRONLY | os.O_CREAT, 0o600)
os.close(fd)
except OSError:
fptr = open(confpath, 'w')
fptr.close()
os.chmod(confpath, 0o600)
else:
if sys.platform != 'win32':
if mode & stat.S_IRGRP or mode & stat.S_IROTH:
raise self.WrongPermissions(
u'Weboob will not start as long as config file %s is readable by group or other users.' % confpath)
def iter_backends(self):
config = RawConfigParser()
config.read(self.confpath)
changed = False
for backend_name in config.sections():
params = dict(config.items(backend_name))
try:
module_name = params.pop('_module')
except KeyError:
try:
module_name = params.pop('_backend')
config.set(backend_name, '_module', module_name)
config.remove_option(backend_name, '_backend')
changed = True
except KeyError:
warning('Missing field "_module" for configured backend "%s"', backend_name)
continue
yield backend_name, module_name, params
if changed:
with open(self.confpath, 'wb') as f:
config.write(f)
def backend_exists(self, name):
"""
Return True if the backend exists in config.
"""
config = RawConfigParser()
config.read(self.confpath)
return name in config.sections()
def add_backend(self, backend_name, module_name, params, edit=False):
if not backend_name:
raise ValueError(u'Please give a name to the configured backend.')
config = RawConfigParser()
config.read(self.confpath)
if not edit:
try:
config.add_section(backend_name)
except DuplicateSectionError:
raise BackendAlreadyExists(backend_name)
config.set(backend_name, '_module', module_name)
for key, value in params.iteritems():
if isinstance(value, unicode):
value = value.encode('utf-8')
config.set(backend_name, key, value)
with open(self.confpath, 'wb') as f:
config.write(f)
def edit_backend(self, backend_name, module_name, params):
return self.add_backend(backend_name, module_name, params, True)
def get_backend(self, backend_name):
config = RawConfigParser()
config.read(self.confpath)
if not config.has_section(backend_name):
raise KeyError(u'Configured backend "%s" not found' % backend_name)
items = dict(config.items(backend_name))
try:
module_name = items.pop('_module')
except KeyError:
try:
module_name = items.pop('_backend')
self.edit_backend(backend_name, module_name, items)
except KeyError:
warning('Missing field "_module" for configured backend "%s"', backend_name)
raise KeyError(u'Configured backend "%s" not found' % backend_name)
return module_name, items
def remove_backend(self, backend_name):
config = RawConfigParser()
config.read(self.confpath)
if not config.remove_section(backend_name):
return False
with open(self.confpath, 'w') as f:
config.write(f)
return True
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/core/bcall.py 0000664 0000000 0000000 00000011650 12411220504 0024223 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from copy import copy
from threading import Thread
try:
import Queue
except ImportError:
import queue as Queue
from weboob.capabilities.base import BaseObject
from weboob.tools.misc import get_backtrace
from weboob.tools.log import getLogger
__all__ = ['BackendsCall', 'CallErrors']
class CallErrors(Exception):
def __init__(self, errors):
msg = 'Errors during backend calls:\n' + \
'\n'.join(['Module(%r): %r\n%r\n' % (backend, error, backtrace)
for backend, error, backtrace in errors])
Exception.__init__(self, msg)
self.errors = copy(errors)
def __iter__(self):
return self.errors.__iter__()
class BackendsCall(object):
def __init__(self, backends, function, *args, **kwargs):
"""
:param backends: List of backends to call
:type backends: list[:class:`Module`]
:param function: backends' method name, or callable object.
:type function: :class:`str` or :class:`callable`
"""
self.logger = getLogger('bcall')
self.responses = Queue.Queue()
self.errors = []
self.tasks = Queue.Queue()
for backend in backends:
Thread(target=self.backend_process, args=(function, args, kwargs)).start()
self.tasks.put(backend)
def store_result(self, backend, result):
if isinstance(result, BaseObject):
result.backend = backend.name
self.responses.put((backend, result))
def backend_process(self, function, args, kwargs):
backend = self.tasks.get()
with backend:
try:
# Call method on backend
try:
self.logger.debug('%s: Calling function %s' % (backend, function))
if callable(function):
result = function(backend, *args, **kwargs)
else:
result = getattr(backend, function)(*args, **kwargs)
except Exception as error:
self.logger.debug('%s: Called function %s raised an error: %r' % (backend, function, error))
self.errors.append((backend, error, get_backtrace(error)))
else:
self.logger.debug('%s: Called function %s returned: %r' % (backend, function, result))
if hasattr(result, '__iter__') and not isinstance(result, basestring):
# Loop on iterator
try:
for subresult in result:
self.store_result(backend, subresult)
except Exception as error:
self.errors.append((backend, error, get_backtrace(error)))
else:
self.store_result(backend, result)
finally:
self.tasks.task_done()
def _callback_thread_run(self, callback, errback):
while self.tasks.unfinished_tasks or not self.responses.empty():
try:
callback(*self.responses.get(timeout=0.1))
except Queue.Empty:
continue
# Raise errors
while self.errors:
errback(*self.errors.pop(0))
callback(None, None)
def callback_thread(self, callback, errback=None):
"""
Call this method to create a thread which will callback a
specified function everytimes a new result comes.
When the process is over, the function will be called with
both arguments set to None.
The functions prototypes:
def callback(backend, result)
def errback(backend, error)
"""
thread = Thread(target=self._callback_thread_run, args=(callback, errback))
thread.start()
return thread
def wait(self):
self.tasks.join()
if self.errors:
raise CallErrors(self.errors)
def __iter__(self):
while self.tasks.unfinished_tasks or not self.responses.empty():
try:
yield self.responses.get(timeout=0.1)
except Queue.Empty:
continue
if self.errors:
raise CallErrors(self.errors)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/core/modules.py 0000664 0000000 0000000 00000014060 12411220504 0024614 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import imp
import logging
from weboob.tools.backend import Module
from weboob.tools.log import getLogger
__all__ = ['LoadedModule', 'ModulesLoader', 'RepositoryModulesLoader', 'ModuleLoadError']
class ModuleLoadError(Exception):
def __init__(self, module_name, msg):
Exception.__init__(self, msg)
self.module = module_name
class LoadedModule(object):
def __init__(self, package):
self.logger = getLogger('backend')
self.package = package
self.klass = None
for attrname in dir(self.package):
attr = getattr(self.package, attrname)
if isinstance(attr, type) and issubclass(attr, Module) and attr != Module:
self.klass = attr
if not self.klass:
raise ImportError('%s is not a backend (no Module class found)' % package)
@property
def name(self):
return self.klass.NAME
@property
def maintainer(self):
return u'%s <%s>' % (self.klass.MAINTAINER, self.klass.EMAIL)
@property
def version(self):
return self.klass.VERSION
@property
def description(self):
return self.klass.DESCRIPTION
@property
def license(self):
return self.klass.LICENSE
@property
def config(self):
return self.klass.CONFIG
@property
def website(self):
if self.klass.BROWSER and hasattr(self.klass.BROWSER, 'BASEURL') and self.klass.BROWSER.BASEURL:
return self.klass.BROWSER.BASEURL
if self.klass.BROWSER and hasattr(self.klass.BROWSER, 'DOMAIN') and self.klass.BROWSER.DOMAIN:
return '%s://%s' % (self.klass.BROWSER.PROTOCOL, self.klass.BROWSER.DOMAIN)
else:
return None
@property
def icon(self):
return self.klass.ICON
def iter_caps(self):
return self.klass.iter_caps()
def has_caps(self, *caps):
for c in caps:
if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \
(type(c) == type and issubclass(self.klass, c)):
return True
return False
def create_instance(self, weboob, instance_name, config, storage):
backend_instance = self.klass(weboob, instance_name, config, storage, self.logger)
self.logger.debug(u'Created backend "%s" for module "%s"' % (instance_name, self.name))
return backend_instance
class ModulesLoader(object):
"""
Load modules.
"""
def __init__(self, path, version=None):
self.version = version
self.path = path
self.loaded = {}
self.logger = getLogger('modules')
def get_or_load_module(self, module_name):
"""
Can raise a ModuleLoadError exception.
"""
if module_name not in self.loaded:
self.load_module(module_name)
return self.loaded[module_name]
def iter_existing_module_names(self):
for name in os.listdir(self.path):
try:
if '__init__.py' in os.listdir(os.path.join(self.path, name)):
yield name
except OSError:
# if path/name is not a directory
continue
def load_all(self):
for existing_module_name in self.iter_existing_module_names():
try:
self.load_module(existing_module_name)
except ModuleLoadError as e:
self.logger.warning(e)
def load_module(self, module_name):
if module_name in self.loaded:
self.logger.debug('Module "%s" is already loaded from %s' % (module_name, self.loaded[module_name].package.__path__[0]))
return
path = self.get_module_path(module_name)
try:
fp, pathname, description = imp.find_module(module_name, [path])
try:
module = LoadedModule(imp.load_module(module_name, fp, pathname, description))
finally:
if fp:
fp.close()
except Exception as e:
if logging.root.level == logging.DEBUG:
self.logger.exception(e)
raise ModuleLoadError(module_name, e)
if module.version != self.version:
raise ModuleLoadError(module_name, "Module requires Weboob %s, but you use Weboob %s. Hint: use 'weboob-config update'"
% (module.version, self.version))
self.loaded[module_name] = module
self.logger.debug('Loaded module "%s" from %s' % (module_name, module.package.__path__[0]))
def get_module_path(self, module_name):
return self.path
class RepositoryModulesLoader(ModulesLoader):
"""
Load modules from repositories.
"""
def __init__(self, repositories):
super(RepositoryModulesLoader, self).__init__(repositories.modules_dir, repositories.version)
self.repositories = repositories
def iter_existing_module_names(self):
for name in self.repositories.get_all_modules_info().iterkeys():
yield name
def get_module_path(self, module_name):
minfo = self.repositories.get_module_info(module_name)
if minfo is None:
raise ModuleLoadError(module_name, 'No such module %s' % module_name)
if minfo.path is None:
raise ModuleLoadError(module_name, 'Module %s is not installed' % module_name)
return minfo.path
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/core/ouiboube.py 0000664 0000000 0000000 00000041170 12411220504 0024757 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
from weboob.core.bcall import BackendsCall
from weboob.core.modules import ModulesLoader, RepositoryModulesLoader, ModuleLoadError
from weboob.core.backendscfg import BackendsConfig
from weboob.core.repositories import Repositories, IProgress
from weboob.core.scheduler import Scheduler
from weboob.tools.backend import Module
from weboob.tools.config.iconfig import ConfigError
from weboob.tools.log import getLogger
__all__ = ['WebNip', 'Weboob']
class VersionsMismatchError(ConfigError):
pass
class WebNip(object):
"""
Weboob in Non Integrated Programs
It provides methods to build backends or call methods on all loaded
backends.
:param modules_path: path to directory containing modules.
:type modules_path: :class:`basestring`
:param storage: provide a storage where backends can save data
:type storage: :class:`weboob.tools.storage.IStorage`
:param scheduler: what scheduler to use; default is :class:`weboob.core.scheduler.Scheduler`
:type scheduler: :class:`weboob.core.scheduler.IScheduler`
"""
VERSION = '1.0'
def __init__(self, modules_path=None, storage=None, scheduler=None):
self.logger = getLogger('weboob')
self.backend_instances = {}
self.callbacks = {'login': lambda backend_name, value: None,
'captcha': lambda backend_name, image: None,
}
if modules_path is None:
import pkg_resources
modules_path = pkg_resources.resource_filename('weboob_modules', '')
if modules_path:
self.modules_loader = ModulesLoader(modules_path, self.VERSION)
if scheduler is None:
scheduler = Scheduler()
self.scheduler = scheduler
self.storage = storage
def __deinit__(self):
self.deinit()
def deinit(self):
"""
Call this method when you stop using Weboob, to
properly unload all correctly.
"""
self.unload_backends()
def build_backend(self, module_name, params=None, storage=None, name=None):
"""
Create a backend.
It does not load it into the Weboob object, so you are responsible for
deinitialization and calls.
:param module_name: name of module
:param params: parameters to give to backend
:type params: :class:`dict`
:param storage: storage to use
:type storage: :class:`weboob.tools.storage.IStorage`
:param name: name of backend
:type name: :class:`basestring`
:rtype: :class:`weboob.tools.backend.Module`
"""
module = self.modules_loader.get_or_load_module(module_name)
backend_instance = module.create_instance(self, name or module_name, params or {}, storage)
return backend_instance
class LoadError(Exception):
"""
Raised when a backend is unabled to load.
:param backend_name: name of backend we can't load
:param exception: exception object
"""
def __init__(self, backend_name, exception):
Exception.__init__(self, unicode(exception))
self.backend_name = backend_name
def load_backend(self, module_name, name, params=None, storage=None):
"""
Load a backend.
:param module_name: name of module to load
:type module_name: :class:`basestring`:
:param name: name of instance
:type name: :class:`basestring`
:param params: parameters to give to backend
:type params: :class:`dict`
:param storage: storage to use
:type storage: :class:`weboob.tools.storage.IStorage`
:rtype: :class:`weboob.tools.backend.Module`
"""
if name is None:
name = module_name
if name in self.backend_instances:
raise self.LoadError(name, 'A loaded backend already named "%s"' % name)
backend = self.build_backend(module_name, params, storage, name)
self.backend_instances[name] = backend
return backend
def unload_backends(self, names=None):
"""
Unload backends.
:param names: if specified, only unload that backends
:type names: :class:`list`
"""
unloaded = {}
if isinstance(names, basestring):
names = [names]
elif names is None:
names = self.backend_instances.keys()
for name in names:
backend = self.backend_instances.pop(name)
with backend:
backend.deinit()
unloaded[backend.name] = backend
return unloaded
def get_backend(self, name, **kwargs):
"""
Get a backend from its name.
:param name: name of backend to get
:type name: str
:param default: if specified, get this value when the backend is not found
:type default: whatever you want
:raises: :class:`KeyError` if not found.
"""
try:
return self.backend_instances[name]
except KeyError:
if 'default' in kwargs:
return kwargs['default']
else:
raise
def count_backends(self):
"""
Get number of loaded backends.
"""
return len(self.backend_instances)
def iter_backends(self, caps=None):
"""
Iter on each backends.
Note: each backend is locked when it is returned.
:param caps: optional list of capabilities to select backends
:type caps: tuple[:class:`weboob.capabilities.base.CapBase`]
:rtype: iter[:class:`weboob.tools.backend.Module`]
"""
for _, backend in sorted(self.backend_instances.iteritems()):
if caps is None or backend.has_caps(caps):
with backend:
yield backend
def __getattr__(self, name):
def caller(*args, **kwargs):
return self.do(name, *args, **kwargs)
return caller
def do(self, function, *args, **kwargs):
r"""
Do calls on loaded backends with specified arguments, in separated
threads.
This function has two modes:
- If *function* is a string, it calls the method with this name on
each backends with the specified arguments;
- If *function* is a callable, it calls it in a separated thread with
the locked backend instance at first arguments, and \*args and
\*\*kwargs.
:param function: backend's method name, or a callable object
:type function: :class:`str`
:param backends: list of backends to iterate on
:type backends: list[:class:`str`]
:param caps: iterate on backends which implement this caps
:type caps: list[:class:`weboob.capabilities.base.CapBase`]
:rtype: A :class:`weboob.core.bcall.BackendsCall` object (iterable)
"""
backends = self.backend_instances.values()
_backends = kwargs.pop('backends', None)
if _backends is not None:
if isinstance(_backends, Module):
backends = [_backends]
elif isinstance(_backends, basestring):
if len(_backends) > 0:
try:
backends = [self.backend_instances[_backends]]
except (ValueError, KeyError):
backends = []
elif isinstance(_backends, (list, tuple, set)):
backends = []
for backend in _backends:
if isinstance(backend, basestring):
try:
backends.append(self.backend_instances[backend])
except (ValueError, KeyError):
pass
else:
backends.append(backend)
else:
self.logger.warning(u'The "backends" value isn\'t supported: %r', _backends)
if 'caps' in kwargs:
caps = kwargs.pop('caps')
backends = [backend for backend in backends if backend.has_caps(caps)]
# The return value MUST BE the BackendsCall instance. Please never iterate
# here on this object, because caller might want to use other methods, like
# wait() on callback_thread().
# Thanks a lot.
return BackendsCall(backends, function, *args, **kwargs)
def schedule(self, interval, function, *args):
"""
Schedule an event.
:param interval: delay before calling the function
:type interval: int
:param function: function to call
:type function: callabale
:param args: arguments to give to function
:returns: an event identificator
"""
return self.scheduler.schedule(interval, function, *args)
def repeat(self, interval, function, *args):
"""
Repeat a call to a function
:param interval: interval between two calls
:type interval: int
:param function: function to call
:type function: callable
:param args: arguments to give to function
:returns: an event identificator
"""
return self.scheduler.repeat(interval, function, *args)
def cancel(self, ev):
"""
Cancel an event
:param ev: the event identificator
"""
return self.scheduler.cancel(ev)
def want_stop(self):
"""
Plan to stop the scheduler.
"""
return self.scheduler.want_stop()
def loop(self):
"""
Run the scheduler loop
"""
return self.scheduler.run()
class Weboob(WebNip):
"""
The main class of Weboob, used to manage backends, modules repositories and
call methods on all loaded backends.
:param workdir: optional parameter to set path of the working directory
:type workdir: str
:param backends_filename: name of the *backends* file, where configuration of
backends is stored
:type backends_filename: str
:param storage: provide a storage where backends can save data
:type storage: :class:`weboob.tools.storage.IStorage`
"""
BACKENDS_FILENAME = 'backends'
def __init__(self, workdir=None, backends_filename=None, scheduler=None, storage=None):
super(Weboob, self).__init__(modules_path=False, scheduler=scheduler, storage=storage)
# Create WORKDIR
if workdir is not None:
datadir = workdir
elif 'WEBOOB_WORKDIR' in os.environ:
datadir = workdir = os.environ.get('WEBOOB_WORKDIR')
else:
workdir = os.path.join(os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config')), 'weboob')
datadir = os.path.join(os.environ.get('XDG_DATA_HOME', os.path.join(os.path.expanduser('~'), '.local', 'share')), 'weboob')
self.workdir = os.path.realpath(workdir)
self._create_dir(workdir)
# Modules management
self.repositories = Repositories(workdir, datadir, self.VERSION)
self.modules_loader = RepositoryModulesLoader(self.repositories)
# Backend instances config
if not backends_filename:
backends_filename = os.environ.get('WEBOOB_BACKENDS', os.path.join(self.workdir, self.BACKENDS_FILENAME))
elif not backends_filename.startswith('/'):
backends_filename = os.path.join(self.workdir, backends_filename)
self.backends_config = BackendsConfig(backends_filename)
def _create_dir(self, name):
if not os.path.exists(name):
os.makedirs(name)
elif not os.path.isdir(name):
self.logger.error(u'"%s" is not a directory', name)
def update(self, progress=IProgress()):
"""
Update modules from repositories.
"""
self.repositories.update(progress)
modules_to_check = set([module_name for _, module_name, _ in self.backends_config.iter_backends()])
for module_name in modules_to_check:
minfo = self.repositories.get_module_info(module_name)
if minfo and not minfo.is_installed():
self.repositories.install(minfo, progress)
def build_backend(self, module_name, params=None, storage=None, name=None):
"""
Create a single backend which is not listed in configuration.
:param module_name: name of module
:param params: parameters to give to backend
:type params: :class:`dict`
:param storage: storage to use
:type storage: :class:`weboob.tools.storage.IStorage`
:param name: name of backend
:type name: :class:`basestring`
:rtype: :class:`weboob.tools.backend.Module`
"""
minfo = self.repositories.get_module_info(module_name)
if minfo is None:
raise ModuleLoadError(module_name, 'Module does not exist.')
if not minfo.is_installed():
self.repositories.install(minfo)
return super(Weboob, self).build_backend(module_name, params, storage, name)
def load_backends(self, caps=None, names=None, modules=None, exclude=None, storage=None, errors=None):
"""
Load backends listed in config file.
:param caps: load backends which implement all of specified caps
:type caps: tuple[:class:`weboob.capabilities.base.CapBase`]
:param names: load backends with instance name in list
:type names: tuple[:class:`str`]
:param modules: load backends which module is in list
:type modules: tuple[:class:`str`]
:param exclude: do not load modules in list
:type exclude: tuple[:class:`str`]
:param storage: use this storage if specified
:type storage: :class:`weboob.tools.storage.IStorage`
:param errors: if specified, store every errors in this list
:type errors: list[:class:`LoadError`]
:returns: loaded backends
:rtype: dict[:class:`str`, :class:`weboob.tools.backend.Module`]
"""
loaded = {}
if storage is None:
storage = self.storage
if not self.repositories.check_repositories():
self.logger.error(u'Repositories are not consistent with the sources.list')
raise VersionsMismatchError(u'Versions mismatch, please run "weboob-config update"')
for instance_name, module_name, params in self.backends_config.iter_backends():
if '_enabled' in params and not params['_enabled'].lower() in ('1', 'y', 'true', 'on', 'yes') or \
names is not None and instance_name not in names or \
modules is not None and module_name not in modules or \
exclude is not None and module_name in exclude:
continue
minfo = self.repositories.get_module_info(module_name)
if minfo is None:
self.logger.warning(u'Backend "%s" is referenced in %s but was not found. '
u'Perhaps a missing repository?', module_name, self.backends_config.confpath)
continue
if caps is not None and not minfo.has_caps(caps):
continue
if not minfo.is_installed():
self.repositories.install(minfo)
module = None
try:
module = self.modules_loader.get_or_load_module(module_name)
except ModuleLoadError as e:
self.logger.error(u'Unable to load module "%s": %s', module_name, e)
continue
if instance_name in self.backend_instances:
self.logger.warning(u'Oops, the backend "%s" is already loaded. Unload it before reloading...', instance_name)
self.unload_backends(instance_name)
try:
backend_instance = module.create_instance(self, instance_name, params, storage)
except Module.ConfigError as e:
if errors is not None:
errors.append(self.LoadError(instance_name, e))
else:
self.backend_instances[instance_name] = loaded[instance_name] = backend_instance
return loaded
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/core/repositories.py 0000664 0000000 0000000 00000065607 12411220504 0025710 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon, Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import imp
import posixpath
import shutil
import re
import sys
import os
import subprocess
from datetime import datetime
from contextlib import closing
from compileall import compile_dir
from io import BytesIO
from weboob.tools.exceptions import BrowserHTTPError, BrowserHTTPNotFound
from .modules import LoadedModule
from weboob.tools.log import getLogger
from weboob.tools.misc import to_unicode
try:
from ConfigParser import RawConfigParser, DEFAULTSECT
except ImportError:
from configparser import RawConfigParser, DEFAULTSECT
__all__ = ['IProgress', 'ModuleInstallError', 'ModuleInfo', 'RepositoryUnavailable',
'Repository', 'Versions', 'Repositories', 'InvalidSignature', 'Keyring']
class ModuleInfo(object):
"""
Information about a module available on a repository.
"""
def __init__(self, name):
self.name = name
# path to the local directory containing this module.
self.path = None
self.url = None
self.repo_url = None
self.version = 0
self.capabilities = ()
self.description = u''
self.maintainer = u''
self.license = u''
self.icon = u''
self.urls = u''
def load(self, items):
self.version = int(items['version'])
self.capabilities = items['capabilities'].split()
self.description = to_unicode(items['description'])
self.maintainer = to_unicode(items['maintainer'])
self.license = to_unicode(items['license'])
self.icon = items['icon'].strip() or None
self.urls = items['urls']
def has_caps(self, caps):
if not isinstance(caps, (list, tuple)):
caps = [caps]
for c in caps:
if type(c) == type:
c = c.__name__
if c in self.capabilities:
return True
return False
def is_installed(self):
return self.path is not None
def is_local(self):
return self.url is None
def dump(self):
return (('version', self.version),
('capabilities', ' '.join(self.capabilities)),
('description', self.description),
('maintainer', self.maintainer),
('license', self.license),
('icon', self.icon or ''),
('urls', self.urls),
)
class RepositoryUnavailable(Exception):
"""
Repository in not available.
"""
class Repository(object):
"""
Represents a repository.
"""
INDEX = 'modules.list'
KEYDIR = '.keys'
KEYRING = 'trusted.gpg'
def __init__(self, url):
self.url = url
self.name = u''
self.update = 0
self.maintainer = u''
self.local = None
self.signed = False
self.key_update = 0
self.modules = {}
if self.url.startswith('file://'):
self.local = True
elif re.match('https?://.*', self.url):
self.local = False
else:
# This is probably a file in ~/.weboob/repositories/, we
# don't know if this is a local or a remote repository.
with open(self.url, 'r') as fp:
self.parse_index(fp)
def __repr__(self):
return '' % self.name
def localurl2path(self):
"""
Get a local path of a file:// URL.
"""
assert self.local is True
if self.url.startswith('file://'):
return self.url[len('file://'):]
return self.url
def retrieve_index(self, browser, repo_path):
"""
Retrieve the index file of this repository. It can use network
if this is a remote repository.
:param repo_path: path to save the downloaded index file.
:type repo_path: str
"""
if self.local:
# Repository is local, open the file.
filename = os.path.join(self.localurl2path(), self.INDEX)
try:
fp = open(filename, 'r')
except IOError as e:
# This local repository doesn't contain a built modules.list index.
self.name = Repositories.url2filename(self.url)
self.build_index(self.localurl2path(), filename)
fp = open(filename, 'r')
else:
# This is a remote repository, download file
try:
fp = BytesIO(browser.open(posixpath.join(self.url, self.INDEX)).content)
except BrowserHTTPError as e:
raise RepositoryUnavailable(unicode(e))
self.parse_index(fp)
if self.local:
# Always rebuild index of a local repository.
self.build_index(self.localurl2path(), filename)
# Save the repository index in ~/.weboob/repositories/
self.save(repo_path, private=True)
def retrieve_keyring(self, browser, keyring_path):
# ignore local
if self.local:
return
keyring = Keyring(keyring_path)
# prevent previously signed repos from going unsigned
if not self.signed and keyring.exists():
raise RepositoryUnavailable('Previously signed repository can not go unsigned')
if not self.signed:
return
if not keyring.exists() or self.key_update > keyring.version:
# This is a remote repository, download file
try:
keyring_data = browser.open(posixpath.join(self.url, self.KEYRING)).content
sig_data = browser.open(posixpath.join(self.url, self.KEYRING + '.sig')).content
except BrowserHTTPError as e:
raise RepositoryUnavailable(unicode(e))
if keyring.exists():
if not keyring.is_valid(keyring_data, sig_data):
raise InvalidSignature('the keyring itself')
print('The keyring was updated (and validated by the previous one).')
else:
print('First time saving the keyring, blindly accepted.')
keyring.save(keyring_data, self.key_update)
print(keyring)
def parse_index(self, fp):
"""
Parse index of a repository
:param fp: file descriptor to read
:type fp: buffer
"""
config = RawConfigParser()
config.readfp(fp)
# Read default parameters
items = dict(config.items(DEFAULTSECT))
try:
self.name = items['name']
self.update = int(items['update'])
self.maintainer = items['maintainer']
self.signed = bool(int(items.get('signed', '0')))
self.key_update = int(items.get('key_update', '0'))
except KeyError as e:
raise RepositoryUnavailable('Missing global parameters in repository: %s' % e)
except ValueError as e:
raise RepositoryUnavailable('Incorrect value in repository parameters: %s' % e)
if len(self.name) == 0:
raise RepositoryUnavailable('Name is empty')
if 'url' in items:
self.url = items['url']
self.local = self.url.startswith('file://')
elif self.local is None:
raise RepositoryUnavailable('Missing "url" key in settings')
# Load modules
self.modules.clear()
for section in config.sections():
module = ModuleInfo(section)
module.load(dict(config.items(section)))
if not self.local:
module.url = posixpath.join(self.url, '%s.tar.gz' % module.name)
module.repo_url = self.url
module.signed = self.signed
self.modules[section] = module
def build_index(self, path, filename):
"""
Rebuild index of modules of repository.
:param path: path of the repository
:type path: str
:param filename: file to save index
:type filename: str
"""
print('Rebuild index')
self.modules.clear()
if os.path.isdir(os.path.join(path, self.KEYDIR)):
self.signed = True
self.key_update = self.get_tree_mtime(os.path.join(path, self.KEYDIR), True)
else:
self.signed = False
self.key_update = 0
for name in sorted(os.listdir(path)):
module_path = os.path.join(path, name)
if not os.path.isdir(module_path) or '.' in name or name == self.KEYDIR:
continue
try:
fp, pathname, description = imp.find_module(name, [path])
try:
module = LoadedModule(imp.load_module(name, fp, pathname, description))
finally:
if fp:
fp.close()
except Exception as e:
print('Unable to build module %s: [%s] %s' % (name, type(e).__name__, e), file=sys.stderr)
else:
m = ModuleInfo(module.name)
m.version = self.get_tree_mtime(module_path)
m.capabilities = list(set([c.__name__ for c in module.iter_caps()]))
m.description = module.description
m.maintainer = module.maintainer
m.license = module.license
m.icon = module.icon or ''
self.modules[module.name] = m
self.update = int(datetime.now().strftime('%Y%m%d%H%M'))
self.save(filename)
@staticmethod
def get_tree_mtime(path, include_root=False):
mtime = 0
if include_root:
mtime = int(datetime.fromtimestamp(os.path.getmtime(path)).strftime('%Y%m%d%H%M'))
for root, dirs, files in os.walk(path):
for f in files:
if f.endswith('.pyc'):
continue
m = int(datetime.fromtimestamp(os.path.getmtime(os.path.join(root, f))).strftime('%Y%m%d%H%M'))
mtime = max(mtime, m)
return mtime
def save(self, filename, private=False):
"""
Save repository into a file (modules.list for example).
:param filename: path to file to save repository.
:type filename: str
:param private: if enabled, save URL of repository.
:type private: bool
"""
config = RawConfigParser()
config.set(DEFAULTSECT, 'name', self.name)
config.set(DEFAULTSECT, 'update', self.update)
config.set(DEFAULTSECT, 'maintainer', self.maintainer)
config.set(DEFAULTSECT, 'signed', int(self.signed))
config.set(DEFAULTSECT, 'key_update', self.key_update)
if private:
config.set(DEFAULTSECT, 'url', self.url)
for module in self.modules.itervalues():
config.add_section(module.name)
for key, value in module.dump():
config.set(module.name, key, to_unicode(value).encode('utf-8'))
with open(filename, 'wb') as f:
config.write(f)
class Versions(object):
VERSIONS_LIST = 'versions.list'
def __init__(self, path):
self.path = path
self.versions = {}
try:
with open(os.path.join(self.path, self.VERSIONS_LIST), 'r') as fp:
config = RawConfigParser()
config.readfp(fp)
# Read default parameters
for key, value in config.items(DEFAULTSECT):
self.versions[key] = int(value)
except IOError:
pass
def get(self, name):
return self.versions.get(name, None)
def set(self, name, version):
self.versions[name] = int(version)
self.save()
def save(self):
config = RawConfigParser()
for name, version in self.versions.iteritems():
config.set(DEFAULTSECT, name, version)
with open(os.path.join(self.path, self.VERSIONS_LIST), 'wb') as fp:
config.write(fp)
class IProgress(object):
def progress(self, percent, message):
print('=== [%3.0f%%] %s' % (percent*100, message))
def error(self, message):
print('ERROR: %s' % message, file=sys.stderr)
class ModuleInstallError(Exception):
pass
DEFAULT_SOURCES_LIST = \
"""# List of Weboob repositories
#
# The entries below override the entries above (with
# backends of the same name).
http://updates.weboob.org/%(version)s/main/
# DEVELOPMENT
# If you want to hack on Weboob modules, you may add a
# reference to sources, for example:
#file:///home/rom1/src/weboob/modules/
"""
class Repositories(object):
SOURCES_LIST = 'sources.list'
MODULES_DIR = 'modules'
REPOS_DIR = 'repositories'
KEYRINGS_DIR = 'keyrings'
ICONS_DIR = 'icons'
SHARE_DIRS = [MODULES_DIR, REPOS_DIR, KEYRINGS_DIR, ICONS_DIR]
def __init__(self, workdir, datadir, version):
self.logger = getLogger('repositories')
self.version = version
self.browser = None
self.workdir = workdir
self.datadir = datadir
self.sources_list = os.path.join(self.workdir, self.SOURCES_LIST)
self.modules_dir = os.path.join(self.datadir, self.MODULES_DIR, self.version)
self.repos_dir = os.path.join(self.datadir, self.REPOS_DIR)
self.keyrings_dir = os.path.join(self.datadir, self.KEYRINGS_DIR)
self.icons_dir = os.path.join(self.datadir, self.ICONS_DIR)
self.create_dir(self.datadir)
self.create_dir(self.modules_dir)
self.create_dir(self.repos_dir)
self.create_dir(self.keyrings_dir)
self.create_dir(self.icons_dir)
self.versions = Versions(self.modules_dir)
self.repositories = []
if not os.path.exists(self.sources_list):
with open(self.sources_list, 'w') as f:
f.write(DEFAULT_SOURCES_LIST)
self.update()
else:
self.load()
def load_browser(self):
from weboob.tools.browser2.browser import BaseBrowser, Weboob as WeboobProfile
class WeboobBrowser(BaseBrowser):
PROFILE = WeboobProfile(self.version)
if self.browser is None:
self.browser = WeboobBrowser()
def create_dir(self, name):
if not os.path.exists(name):
os.makedirs(name)
elif not os.path.isdir(name):
self.logger.error(u'"%s" is not a directory' % name)
def _extend_module_info(self, repo, info):
if repo.local:
info.path = repo.localurl2path()
elif self.versions.get(info.name) is not None:
info.path = self.modules_dir
return info
def get_all_modules_info(self, caps=None):
"""
Get all ModuleInfo instances available.
:param caps: filter on capabilities:
:type caps: list[str]
:rtype: dict[:class:`ModuleInfo`]
"""
modules = {}
for repos in reversed(self.repositories):
for name, info in repos.modules.iteritems():
if not name in modules and (not caps or info.has_caps(caps)):
modules[name] = self._extend_module_info(repos, info)
return modules
def get_module_info(self, name):
"""
Get ModuleInfo object of a module.
It tries all repositories from last to first, and set
the 'path' attribute of ModuleInfo if it is installed.
"""
for repos in reversed(self.repositories):
if name in repos.modules:
m = repos.modules[name]
self._extend_module_info(repos, m)
return m
return None
def load(self):
"""
Load repositories from ~/.local/share/weboob/repositories/.
"""
self.repositories = []
for name in sorted(os.listdir(self.repos_dir)):
path = os.path.join(self.repos_dir, name)
try:
repository = Repository(path)
self.repositories.append(repository)
except RepositoryUnavailable as e:
print('Unable to load repository %s (%s), try to update repositories.' % (name, e), file=sys.stderr)
def get_module_icon_path(self, module):
return os.path.join(self.icons_dir, '%s.png' % module.name)
def retrieve_icon(self, module):
"""
Retrieve the icon of a module and save it in ~/.local/share/weboob/icons/.
"""
self.load_browser()
if not isinstance(module, ModuleInfo):
module = self.get_module_info(module)
dest_path = self.get_module_icon_path(module)
icon_url = module.icon
if not icon_url:
if module.is_local():
icon_path = os.path.join(module.path, module.name, 'favicon.png')
if module.path and os.path.exists(icon_path):
shutil.copy(icon_path, dest_path)
return
else:
icon_url = module.url.replace('.tar.gz', '.png')
try:
icon = self.browser.open(icon_url)
except BrowserHTTPNotFound:
pass # no icon, no problem
else:
with open(dest_path, 'wb') as fp:
fp.write(icon.content)
def _parse_source_list(self):
l = []
with open(self.sources_list, 'r') as f:
for line in f:
line = line.strip() % {'version': self.version}
m = re.match('(file|https?)://.*', line)
if m:
l.append(line)
return l
def update_repositories(self, progress=IProgress()):
self.load_browser()
"""
Update list of repositories by downloading them
and put them in ~/.local/share/weboob/repositories/.
:param progress: observer object.
:type progress: :class:`IProgress`
"""
self.repositories = []
for name in os.listdir(self.repos_dir):
os.remove(os.path.join(self.repos_dir, name))
gpgv = Keyring.find_gpgv()
for line in self._parse_source_list():
progress.progress(0.0, 'Getting %s' % line)
repository = Repository(line)
filename = self.url2filename(repository.url)
prio_filename = '%02d-%s' % (len(self.repositories), filename)
repo_path = os.path.join(self.repos_dir, prio_filename)
keyring_path = os.path.join(self.keyrings_dir, filename)
try:
repository.retrieve_index(self.browser, repo_path)
if gpgv:
repository.retrieve_keyring(self.browser, keyring_path)
else:
progress.error('Cannot find gpgv to check for repository authenticity.\n'
'You should install GPG for better security.')
except RepositoryUnavailable as e:
progress.error('Unable to load repository: %s' % e)
else:
self.repositories.append(repository)
def check_repositories(self):
"""
Check if sources.list is consistent with repositories
"""
l = []
for line in self._parse_source_list():
repository = Repository(line)
filename = self.url2filename(repository.url)
prio_filename = '%02d-%s' % (len(l), filename)
repo_path = os.path.join(self.repos_dir, prio_filename)
if not os.path.isfile(repo_path):
return False
l.append(repository)
return True
def update(self, progress=IProgress()):
"""
Update repositories and install new packages versions.
:param progress: observer object.
:type progress: :class:`IProgress`
"""
self.update_repositories()
to_update = []
for name, info in self.get_all_modules_info().iteritems():
if not info.is_local() and info.is_installed():
to_update.append(info)
class InstallProgress(IProgress):
def __init__(self, n):
self.n = n
def progress(self, percent, message):
progress.progress(float(self.n)/len(to_update) + 1.0/len(to_update)*percent, message)
for n, info in enumerate(to_update):
inst_progress = InstallProgress(n)
try:
self.install(info, inst_progress)
except ModuleInstallError as e:
inst_progress.progress(1.0, unicode(e))
def install(self, module, progress=IProgress()):
"""
Install a module.
:param module: module to install
:type module: :class:`str` or :class:`ModuleInfo`
:param progress: observer object
:type progress: :class:`IProgress`
"""
import tarfile
self.load_browser()
if isinstance(module, ModuleInfo):
info = module
elif isinstance(module, basestring):
progress.progress(0.0, 'Looking for module %s' % module)
info = self.get_module_info(module)
if not info:
raise ModuleInstallError('Module "%s" does not exist' % module)
else:
raise ValueError('"module" parameter might be a ModuleInfo object or a string, not %r' % module)
module = info
if module.is_local():
raise ModuleInstallError('%s is available on local.' % module.name)
module_dir = os.path.join(self.modules_dir, module.name)
installed = self.versions.get(module.name)
if installed is None or not os.path.exists(module_dir):
progress.progress(0.3, 'Module %s is not installed yet' % module.name)
elif module.version > installed:
progress.progress(0.3, 'A new version of %s is available' % module.name)
else:
raise ModuleInstallError('The latest version of %s is already installed' % module.name)
progress.progress(0.2, 'Downloading module...')
try:
tardata = self.browser.open(module.url).content
except BrowserHTTPError as e:
raise ModuleInstallError('Unable to fetch module: %s' % e)
# Check signature
if module.signed and Keyring.find_gpgv():
progress.progress(0.5, 'Checking module authenticity...')
sig_data = self.browser.open(posixpath.join(module.url + '.sig')).content
keyring_path = os.path.join(self.keyrings_dir, self.url2filename(module.repo_url))
keyring = Keyring(keyring_path)
if not keyring.exists():
raise ModuleInstallError('No keyring found, please update repos.')
if not keyring.is_valid(tardata, sig_data):
raise ModuleInstallError('Invalid signature for %s.' % module.name)
# Extract module from tarball.
if os.path.isdir(module_dir):
shutil.rmtree(module_dir)
progress.progress(0.7, 'Setting up module...')
with closing(tarfile.open('', 'r:gz', BytesIO(tardata))) as tar:
tar.extractall(self.modules_dir)
if not os.path.isdir(module_dir):
raise ModuleInstallError('The archive for %s looks invalid.' % module.name)
# Precompile
compile_dir(module_dir, quiet=True)
self.versions.set(module.name, module.version)
progress.progress(0.9, 'Downloading icon...')
self.retrieve_icon(module)
progress.progress(1.0, 'Module %s has been installed!' % module.name)
@staticmethod
def url2filename(url):
"""
Get a safe file name for an URL.
All non-alphanumeric characters are replaced by _.
"""
return ''.join([l if l.isalnum() else '_' for l in url])
class InvalidSignature(Exception):
def __init__(self, filename):
self.filename = filename
Exception.__init__(self, 'Invalid signature for %s' % filename)
class Keyring(object):
EXTENSION = '.gpg'
def __init__(self, path):
self.path = path + self.EXTENSION
self.vpath = path + '.version'
self.version = 0
if self.exists():
with open(self.vpath, 'r') as f:
self.version = int(f.read().strip())
else:
if os.path.exists(self.path):
os.remove(self.path)
if os.path.exists(self.vpath):
os.remove(self.vpath)
def exists(self):
if not os.path.exists(self.vpath):
return False
if os.path.exists(self.path):
# Check the file is not empty.
# This is because there was a bug creating empty keyring files.
with open(self.path, 'r') as fp:
if len(fp.read().strip()):
return True
return False
def save(self, keyring_data, version):
with open(self.path, 'wb') as fp:
fp.write(keyring_data)
self.version = version
with open(self.vpath, 'wb') as fp:
fp.write(str(version))
@staticmethod
def find_gpgv():
if os.getenv('GPGV_EXECUTABLE'):
return os.getenv('GPGV_EXECUTABLE')
paths = os.getenv('PATH', os.defpath).split(os.pathsep)
for path in paths:
for ex in ('gpgv2', 'gpgv'):
fpath = os.path.join(path, ex)
if os.path.exists(fpath) and os.access(fpath, os.X_OK):
return fpath
def is_valid(self, data, sigdata):
"""
Check if the data is signed by an accepted key.
data and sigdata should be strings.
"""
gpgv = self.find_gpgv()
from tempfile import NamedTemporaryFile
with NamedTemporaryFile(suffix='.sig') as sigfile:
sigfile.write(sigdata)
sigfile.flush() # very important
assert isinstance(data, basestring)
# Yes, all of it is necessary
proc = subprocess.Popen([gpgv,
'--status-fd', '1',
'--keyring', os.path.realpath(self.path),
os.path.realpath(sigfile.name),
'-'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = proc.communicate(data)
if proc.returncode or 'GOODSIG' not in out or 'VALIDSIG' not in out:
print(out, err, file=sys.stderr)
return False
return True
def __str__(self):
if self.exists():
with open(self.vpath, 'r') as f:
import hashlib
h = hashlib.sha1(f.read()).hexdigest()
return 'Keyring version %s, checksum %s' % (self.version, h)
return 'NO KEYRING'
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/core/scheduler.py 0000664 0000000 0000000 00000010362 12411220504 0025123 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from threading import Event, RLock
try:
from threading import _Timer as Timer
except ImportError:
from threading import Timer
from weboob.tools.log import getLogger
from weboob.tools.misc import get_backtrace
__all__ = ['Scheduler']
class IScheduler(object):
def schedule(self, interval, function, *args):
raise NotImplementedError()
def repeat(self, interval, function, *args):
raise NotImplementedError()
def cancel(self, ev):
raise NotImplementedError()
def run(self):
raise NotImplementedError()
def want_stop(self):
raise NotImplementedError()
class RepeatedTimer(Timer):
def run(self):
while not self.finished.isSet():
try:
self.function(*self.args, **self.kwargs)
except Exception:
# do not stop timer because of an exception
print(get_backtrace())
self.finished.wait(self.interval)
self.finished.set()
class Scheduler(IScheduler):
def __init__(self):
self.logger = getLogger('scheduler')
self.mutex = RLock()
self.stop_event = Event()
self.count = 0
self.queue = {}
def schedule(self, interval, function, *args):
return self._schedule(Timer, interval, self._schedule_callback, function, *args)
def repeat(self, interval, function, *args):
return self._schedule(RepeatedTimer, interval, self._repeat_callback, function, *args)
def _schedule(self, klass, interval, meta_func, function, *args):
if self.stop_event.isSet():
return
with self.mutex:
self.count += 1
self.logger.debug('function "%s" will be called in %s seconds' % (function.__name__, interval))
timer = klass(interval, meta_func, (self.count, interval, function, args))
self.queue[self.count] = timer
timer.start()
return self.count
def _schedule_callback(self, count, interval, function, args):
with self.mutex:
self.queue.pop(count)
return function(*args)
def _repeat_callback(self, count, interval, function, args):
function(*args)
with self.mutex:
try:
e = self.queue[count]
except KeyError:
return
else:
self.logger.debug('function "%s" will be called in %s seconds' % (function.__name__, e.interval))
def cancel(self, ev):
with self.mutex:
try:
e = self.queue.pop(ev)
except KeyError:
return False
e.cancel()
self.logger.debug('scheduled function "%s" is canceled' % e.function.__name__)
return True
def _wait_to_stop(self):
self.want_stop()
with self.mutex:
for e in self.queue.itervalues():
e.cancel()
e.join()
self.queue = {}
def run(self):
try:
while True:
self.stop_event.wait(0.1)
except KeyboardInterrupt:
self._wait_to_stop()
raise
else:
self._wait_to_stop()
return True
def want_stop(self):
self.stop_event.set()
with self.mutex:
for t in self.queue.itervalues():
t.cancel()
# Contrary to _wait_to_stop(), don't call t.join
# because want_stop() have to be non-blocking.
self.queue = {}
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/ 0000775 0000000 0000000 00000000000 12411220504 0023001 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0025100 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/ 0000775 0000000 0000000 00000000000 12411220504 0025304 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/__init__.py 0000664 0000000 0000000 00000000000 12411220504 0027403 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/base.py 0000664 0000000 0000000 00000040617 12411220504 0026600 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon, Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import logging
import optparse
from optparse import OptionGroup, OptionParser
import locale
import os
import sys
import warnings
from weboob.capabilities.base import ConversionWarning, BaseObject
from weboob.core import Weboob, CallErrors
from weboob.core.backendscfg import BackendsConfig
from weboob.tools.config.iconfig import ConfigError
from weboob.tools.exceptions import FormFieldConversionWarning
from weboob.tools.log import createColoredFormatter, getLogger, settings as log_settings
from weboob.tools.misc import to_unicode
from .results import ResultsConditionError
__all__ = ['Application']
class MoreResultsAvailable(Exception):
pass
class ApplicationStorage(object):
def __init__(self, name, storage):
self.name = name
self.storage = storage
def set(self, *args):
if self.storage:
return self.storage.set('applications', self.name, *args)
def delete(self, *args):
if self.storage:
return self.storage.delete('applications', self.name, *args)
def get(self, *args, **kwargs):
if self.storage:
return self.storage.get('applications', self.name, *args, **kwargs)
else:
return kwargs.get('default', None)
def load(self, default):
if self.storage:
return self.storage.load('applications', self.name, default)
def save(self):
if self.storage:
return self.storage.save('applications', self.name)
class Application(object):
"""
Base application.
This class can be herited to have some common code within weboob
applications.
"""
# ------ Class attributes --------------------------------------
# Application name
APPNAME = ''
# Configuration and work directory (if None, use the Weboob instance one)
CONFDIR = None
# Default configuration dict (can only contain key/values)
CONFIG = {}
# Default storage tree
STORAGE = {}
# Synopsis
SYNOPSIS = 'Usage: %prog [-h] [-dqv] [-b backends] ...\n'
SYNOPSIS += ' %prog [--help] [--version]'
# Description
DESCRIPTION = None
# Version
VERSION = None
# Copyright
COPYRIGHT = None
stdin = sys.stdin
stdout = sys.stdout
stderr = sys.stderr
# ------ Abstract methods --------------------------------------
def create_weboob(self):
return Weboob()
def _get_completions(self):
"""
Overload this method in subclasses if you want to enrich shell completion.
@return a set object
"""
return set()
def _handle_options(self):
"""
Overload this method in application type subclass
if you want to handle options defined in subclass constructor.
"""
pass
def add_application_options(self, group):
"""
Overload this method if your application needs extra options.
These options will be displayed in an option group.
"""
pass
def handle_application_options(self):
"""
Overload this method in your application if you want to handle options defined in add_application_options.
"""
pass
# ------ Application methods -------------------------------
def __init__(self, option_parser=None):
self.encoding = self.guess_encoding()
self.logger = getLogger(self.APPNAME)
self.weboob = self.create_weboob()
if self.CONFDIR is None:
self.CONFDIR = self.weboob.workdir
self.config = None
self.options = None
self.condition = None
self.storage = None
if option_parser is None:
self._parser = OptionParser(self.SYNOPSIS, version=self._get_optparse_version())
else:
self._parser = option_parser
if self.DESCRIPTION:
self._parser.description = self.DESCRIPTION
app_options = OptionGroup(self._parser, '%s Options' % self.APPNAME.capitalize())
self.add_application_options(app_options)
if len(app_options.option_list) > 0:
self._parser.add_option_group(app_options)
self._parser.add_option('-b', '--backends', help='what backend(s) to enable (comma separated)')
self._parser.add_option('-e', '--exclude-backends', help='what backend(s) to exclude (comma separated)')
self._parser.add_option('-I', '--insecure', action='store_true', help='do not validate SSL')
logging_options = OptionGroup(self._parser, 'Logging Options')
logging_options.add_option('-d', '--debug', action='store_true', help='display debug messages')
logging_options.add_option('-q', '--quiet', action='store_true', help='display only error messages')
logging_options.add_option('-v', '--verbose', action='store_true', help='display info messages')
logging_options.add_option('--logging-file', action='store', type='string', dest='logging_file', help='file to save logs')
logging_options.add_option('-a', '--save-responses', action='store_true', help='save every response')
self._parser.add_option_group(logging_options)
self._parser.add_option('--shell-completion', action='store_true', help=optparse.SUPPRESS_HELP)
self._is_default_count = True
def guess_encoding(self, stdio=None):
if stdio is None:
stdio = self.stdout
encoding = stdio.encoding or locale.getpreferredencoding()
# ASCII or ANSII is most likely a user mistake
if not encoding or encoding.lower() == 'ascii' or encoding.lower().startswith('ansi'):
encoding = 'UTF-8'
return encoding
def deinit(self):
self.weboob.want_stop()
self.weboob.deinit()
def create_storage(self, path=None, klass=None, localonly=False):
"""
Create a storage object.
:param path: An optional specific path
:type path: :class:`str`
:param klass: What class to instance
:type klass: :class:`weboob.tools.storage.IStorage`
:param localonly: If True, do not set it on the :class:`Weboob` object.
:type localonly: :class:`bool`
:rtype: :class:`weboob.tools.storage.IStorage`
"""
if klass is None:
from weboob.tools.storage import StandardStorage
klass = StandardStorage
if path is None:
path = os.path.join(self.CONFDIR, self.APPNAME + '.storage')
elif not os.path.sep in path:
path = os.path.join(self.CONFDIR, path)
storage = klass(path)
self.storage = ApplicationStorage(self.APPNAME, storage)
self.storage.load(self.STORAGE)
if not localonly:
self.weboob.storage = storage
return storage
def load_config(self, path=None, klass=None):
"""
Load a configuration file and get his object.
:param path: An optional specific path
:type path: :class:`str`
:param klass: What class to instance
:type klass: :class:`weboob.tools.config.iconfig.IConfig`
:rtype: :class:`weboob.tools.config.iconfig.IConfig`
"""
if klass is None:
from weboob.tools.config.iniconfig import INIConfig
klass = INIConfig
if path is None:
path = os.path.join(self.CONFDIR, self.APPNAME)
elif not os.path.sep in path:
path = os.path.join(self.CONFDIR, path)
self.config = klass(path)
self.config.load(self.CONFIG)
def main(self, argv):
"""
Main method
Called by run
"""
raise NotImplementedError()
def load_backends(self, caps=None, names=None, exclude=None, *args, **kwargs):
if names is None and self.options.backends:
names = self.options.backends.split(',')
if exclude is None and self.options.exclude_backends:
exclude = self.options.exclude_backends.split(',')
loaded = self.weboob.load_backends(caps, names, exclude=exclude, *args, **kwargs)
if not loaded:
logging.info(u'No backend loaded')
return loaded
def _get_optparse_version(self):
version = None
if self.VERSION:
if self.COPYRIGHT:
version = '%s v%s %s' % (self.APPNAME, self.VERSION, self.COPYRIGHT)
else:
version = '%s v%s' % (self.APPNAME, self.VERSION)
return version
def _do_complete_obj(self, backend, fields, obj):
if not obj:
return obj
if not isinstance(obj, BaseObject):
return obj
obj.backend = backend.name
if fields is None or len(fields) > 0:
backend.fillobj(obj, fields)
return obj
def _do_complete_iter(self, backend, count, fields, res):
modif = 0
for i, sub in enumerate(res):
sub = self._do_complete_obj(backend, fields, sub)
if self.condition and not self.condition.is_valid(sub):
modif += 1
else:
if count and i - modif == count:
if self._is_default_count:
raise MoreResultsAvailable()
else:
return
yield sub
def _do_complete(self, backend, count, selected_fields, function, *args, **kwargs):
assert count is None or count > 0
if callable(function):
res = function(backend, *args, **kwargs)
else:
res = getattr(backend, function)(*args, **kwargs)
if hasattr(res, '__iter__'):
return self._do_complete_iter(backend, count, selected_fields, res)
else:
return self._do_complete_obj(backend, selected_fields, res)
def bcall_error_handler(self, backend, error, backtrace):
"""
Handler for an exception inside the CallErrors exception.
This method can be overrided to support more exceptions types.
"""
# Ignore this error.
if isinstance(error, MoreResultsAvailable):
return False
print(u'Error(%s): %s' % (backend.name, error), file=self.stderr)
if logging.root.level == logging.DEBUG:
print(backtrace, file=self.stderr)
else:
return True
def bcall_errors_handler(self, errors, debugmsg='Use --debug option to print backtraces', ignore=()):
"""
Handler for the CallErrors exception.
It calls `bcall_error_handler` for each error.
:param errors: Object containing errors from backends
:type errors: :class:`weboob.core.bcall.CallErrors`
:param debugmsg: Default message asking to enable the debug mode
:type debugmsg: :class:`basestring`
:param ignore: Exceptions to ignore
:type ignore: tuple[:class:`Exception`]
"""
ask_debug_mode = False
for backend, error, backtrace in errors.errors:
if isinstance(error, ignore):
continue
elif self.bcall_error_handler(backend, error, backtrace):
ask_debug_mode = True
if ask_debug_mode:
print(debugmsg, file=self.stderr)
def parse_args(self, args):
self.options, args = self._parser.parse_args(args)
if self.options.shell_completion:
items = set()
for option in self._parser.option_list:
if not option.help is optparse.SUPPRESS_HELP:
items.update(str(option).split('/'))
items.update(self._get_completions())
print(' '.join(items))
sys.exit(0)
if self.options.debug or self.options.save_responses:
level = logging.DEBUG
elif self.options.verbose:
level = logging.INFO
elif self.options.quiet:
level = logging.ERROR
else:
level = logging.WARNING
if self.options.insecure:
log_settings['ssl_insecure'] = True
# this only matters to developers
if not self.options.debug and not self.options.save_responses:
warnings.simplefilter('ignore', category=ConversionWarning)
warnings.simplefilter('ignore', category=FormFieldConversionWarning)
handlers = []
if self.options.save_responses:
import tempfile
responses_dirname = tempfile.mkdtemp(prefix='weboob_session_')
print('Debug data will be saved in this directory: %s' % responses_dirname, file=self.stderr)
log_settings['save_responses'] = True
log_settings['responses_dirname'] = responses_dirname
handlers.append(self.create_logging_file_handler(os.path.join(responses_dirname, 'debug.log')))
# file logger
if self.options.logging_file:
handlers.append(self.create_logging_file_handler(self.options.logging_file))
else:
handlers.append(self.create_default_logger())
self.setup_logging(level, handlers)
self._handle_options()
self.handle_application_options()
return args
@classmethod
def create_default_logger(cls):
# stdout logger
format = '%(asctime)s:%(levelname)s:%(name)s:' + cls.VERSION +\
':%(filename)s:%(lineno)d:%(funcName)s %(message)s'
handler = logging.StreamHandler(cls.stdout)
handler.setFormatter(createColoredFormatter(cls.stdout, format))
return handler
@classmethod
def setup_logging(cls, level, handlers):
logging.root.handlers = []
logging.root.setLevel(level)
for handler in handlers:
logging.root.addHandler(handler)
def create_logging_file_handler(self, filename):
try:
stream = open(filename, 'w')
except IOError as e:
self.logger.error('Unable to create the logging file: %s' % e)
sys.exit(1)
else:
format = '%(asctime)s:%(levelname)s:%(name)s:' + self.VERSION +\
':%(filename)s:%(lineno)d:%(funcName)s %(message)s'
handler = logging.StreamHandler(stream)
handler.setFormatter(logging.Formatter(format))
return handler
@classmethod
def run(cls, args=None):
"""
This static method can be called to run the application.
It creates the application object, handles options, setups logging, calls
the main() method, and catches common exceptions.
You can't do anything after this call, as it *always* finishes with
a call to sys.exit().
For example:
>>> from weboob.application.myapplication import MyApplication
>>> MyApplication.run()
"""
cls.setup_logging(logging.INFO, [cls.create_default_logger()])
if args is None:
args = [(cls.stdin.encoding and isinstance(arg, bytes) and arg.decode(cls.stdin.encoding) or to_unicode(arg)) for arg in sys.argv]
try:
app = cls()
except BackendsConfig.WrongPermissions as e:
print(e, file=cls.stderr)
sys.exit(1)
try:
try:
args = app.parse_args(args)
sys.exit(app.main(args))
except KeyboardInterrupt:
print('Program killed by SIGINT', file=cls.stderr)
sys.exit(0)
except EOFError:
sys.exit(0)
except ConfigError as e:
print('Configuration error: %s' % e, file=cls.stderr)
sys.exit(1)
except CallErrors as e:
try:
app.bcall_errors_handler(e)
except KeyboardInterrupt:
pass
sys.exit(1)
except ResultsConditionError as e:
print('%s' % e, file=cls.stderr)
sys.exit(1)
finally:
app.deinit()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/console.py 0000664 0000000 0000000 00000057265 12411220504 0027337 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Christophe Benz, Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
from copy import copy
import getpass
import logging
import subprocess
import sys
import os
from weboob.capabilities import UserError
from weboob.capabilities.account import CapAccount, Account, AccountRegisterError
from weboob.core.backendscfg import BackendAlreadyExists
from weboob.core.modules import ModuleLoadError
from weboob.core.repositories import ModuleInstallError
from weboob.tools.exceptions import BrowserUnavailable, BrowserIncorrectPassword, BrowserForbidden, BrowserSSLError
from weboob.tools.value import Value, ValueBool, ValueFloat, ValueInt, ValueBackendPassword
from weboob.tools.misc import to_unicode
from weboob.tools.ordereddict import OrderedDict
from .base import Application, MoreResultsAvailable
__all__ = ['ConsoleApplication', 'BackendNotGiven']
class BackendNotGiven(Exception):
def __init__(self, id, backends):
self.id = id
self.backends = sorted(backends)
Exception.__init__(self, 'Please specify a backend to use for this argument (%s@backend_name). '
'Availables: %s.' % (id, ', '.join(name for name, backend in backends)))
class BackendNotFound(Exception):
pass
class ConsoleApplication(Application):
"""
Base application class for CLI applications.
"""
CAPS = None
# shell escape strings
if sys.platform == 'win32' \
or not os.isatty(sys.stdout.fileno()) \
or os.getenv('ANSI_COLORS_DISABLED') is not None:
#workaround to disable bold
BOLD = ''
NC = '' # no color
else:
BOLD = '[1m'
NC = '[0m' # no color
def __init__(self, option_parser=None):
Application.__init__(self, option_parser)
self.weboob.callbacks['login'] = self.login_cb
self.enabled_backends = set()
def login_cb(self, backend_name, value):
return self.ask('[%s] %s' % (backend_name,
value.label),
masked=True,
default='',
regexp=value.regexp)
def unload_backends(self, *args, **kwargs):
unloaded = self.weboob.unload_backends(*args, **kwargs)
for backend in unloaded.itervalues():
try:
self.enabled_backends.remove(backend)
except KeyError:
pass
return unloaded
def is_module_loadable(self, info):
return self.CAPS is None or info.has_caps(self.CAPS)
def load_backends(self, *args, **kwargs):
if 'errors' in kwargs:
errors = kwargs['errors']
else:
kwargs['errors'] = errors = []
ret = super(ConsoleApplication, self).load_backends(*args, **kwargs)
for err in errors:
print('Error(%s): %s' % (err.backend_name, err), file=self.stderr)
if self.ask('Do you want to reconfigure this backend?', default=True):
self.edit_backend(err.backend_name)
self.load_backends(names=[err.backend_name])
for name, backend in ret.iteritems():
self.enabled_backends.add(backend)
self.check_loaded_backends()
return ret
def check_loaded_backends(self, default_config=None):
while len(self.enabled_backends) == 0:
print('Warning: there is currently no configured backend for %s' % self.APPNAME)
if not os.isatty(self.stdout.fileno()) or not self.ask('Do you want to configure backends?', default=True):
return False
self.prompt_create_backends(default_config)
return True
def prompt_create_backends(self, default_config=None):
r = ''
while r != 'q':
modules = []
print('\nAvailable modules:')
for name, info in sorted(self.weboob.repositories.get_all_modules_info().iteritems()):
if not self.is_module_loadable(info):
continue
modules.append(name)
loaded = ' '
for bi in self.weboob.iter_backends():
if bi.NAME == name:
if loaded == ' ':
loaded = 'X'
elif loaded == 'X':
loaded = 2
else:
loaded += 1
print('%s%d)%s [%s] %s%-15s%s %s' % (self.BOLD, len(modules), self.NC, loaded,
self.BOLD, name, self.NC, info.description))
print('%sa) --all--%s install all backends' % (self.BOLD, self.NC))
print('%sq)%s --stop--\n' % (self.BOLD, self.NC))
r = self.ask('Select a backend to create (q to stop)', regexp='^(\d+|q|a)$')
if str(r).isdigit():
i = int(r) - 1
if i < 0 or i >= len(modules):
print('Error: %s is not a valid choice' % r, file=self.stderr)
continue
name = modules[i]
try:
inst = self.add_backend(name, default_config)
if inst:
self.load_backends(names=[inst])
except (KeyboardInterrupt, EOFError):
print('\nAborted.')
elif r == 'a':
try:
for name in modules:
if name in [b.NAME for b in self.weboob.iter_backends()]:
continue
inst = self.add_backend(name, default_config)
if inst:
self.load_backends(names=[inst])
except (KeyboardInterrupt, EOFError):
print('\nAborted.')
else:
break
print('Right right!')
def _handle_options(self):
self.load_default_backends()
def load_default_backends(self):
"""
By default loads all backends.
Applications can overload this method to restrict backends loaded.
"""
if len(self.STORAGE) > 0:
self.load_backends(self.CAPS, storage=self.create_storage())
else:
self.load_backends(self.CAPS)
@classmethod
def run(klass, args=None):
try:
super(ConsoleApplication, klass).run(args)
except BackendNotFound as e:
print('Error: Backend "%s" not found.' % e)
sys.exit(1)
def do(self, function, *args, **kwargs):
if not 'backends' in kwargs:
kwargs['backends'] = self.enabled_backends
return self.weboob.do(function, *args, **kwargs)
def parse_id(self, _id, unique_backend=False):
try:
_id, backend_name = _id.rsplit('@', 1)
except ValueError:
backend_name = None
backends = [(b.name, b) for b in self.enabled_backends]
if unique_backend and not backend_name:
if len(backends) == 1:
backend_name = backends[0][0]
else:
raise BackendNotGiven(_id, backends)
if backend_name is not None and not backend_name in dict(backends):
# Is the backend a short version of a real one?
found = False
for key in dict(backends):
if backend_name in key:
# two choices, ambiguous command
if found:
raise BackendNotFound(backend_name)
else:
found = True
_back = key
if found:
return _id, _back
raise BackendNotFound(backend_name)
return _id, backend_name
# user interaction related methods
def register_backend(self, name, ask_add=True):
try:
backend = self.weboob.modules_loader.get_or_load_module(name)
except ModuleLoadError as e:
backend = None
if not backend:
print('Backend "%s" does not exist.' % name, file=self.stderr)
return 1
if not backend.has_caps(CapAccount) or backend.klass.ACCOUNT_REGISTER_PROPERTIES is None:
print('You can\'t register a new account with %s' % name, file=self.stderr)
return 1
account = Account()
account.properties = {}
if backend.website:
website = 'on website %s' % backend.website
else:
website = 'with backend %s' % backend.name
while True:
asked_config = False
for key, prop in backend.klass.ACCOUNT_REGISTER_PROPERTIES.iteritems():
if not asked_config:
asked_config = True
print('Configuration of new account %s' % website)
print('-----------------------------%s' % ('-' * len(website)))
p = copy(prop)
p.set(self.ask(prop, default=account.properties[key].get() if (key in account.properties) else prop.default))
account.properties[key] = p
if asked_config:
print('-----------------------------%s' % ('-' * len(website)))
try:
backend.klass.register_account(account)
except AccountRegisterError as e:
print(u'%s' % e)
if self.ask('Do you want to try again?', default=True):
continue
else:
return None
else:
break
backend_config = {}
for key, value in account.properties.iteritems():
if key in backend.config:
backend_config[key] = value.get()
if ask_add and self.ask('Do you want to add the new register account?', default=True):
return self.add_backend(name, backend_config, ask_register=False)
return backend_config
def install_module(self, name):
try:
self.weboob.repositories.install(name)
except ModuleInstallError as e:
print('Unable to install module "%s": %s' % (name, e), file=self.stderr)
return False
print('')
return True
def edit_backend(self, name, params=None):
return self.add_backend(name, params, True)
def add_backend(self, name, params=None, edit=False, ask_register=True):
if params is None:
params = {}
module = None
config = None
try:
if not edit:
minfo = self.weboob.repositories.get_module_info(name)
if minfo is None:
raise ModuleLoadError(name, 'Module does not exist')
if not minfo.is_installed():
print('Module "%s" is available but not installed.' % minfo.name)
self.install_module(minfo)
module = self.weboob.modules_loader.get_or_load_module(name)
config = module.config
else:
bname, items = self.weboob.backends_config.get_backend(name)
module = self.weboob.modules_loader.get_or_load_module(bname)
items.update(params)
params = items
config = module.config.load(self.weboob, bname, name, params, nofail=True)
except ModuleLoadError as e:
print('Unable to load module "%s": %s' % (name, e), file=self.stderr)
return 1
# ask for params non-specified on command-line arguments
asked_config = False
for key, value in config.iteritems():
if not asked_config:
asked_config = True
print('')
print('Configuration of backend %s' % module.name)
print('-------------------------%s' % ('-' * len(module.name)))
if key not in params or edit:
params[key] = self.ask(value, default=params[key] if (key in params) else value.default)
else:
print(u' [%s] %s: %s' % (key, value.description, '(masked)' if value.masked else to_unicode(params[key])))
if asked_config:
print('-------------------------%s' % ('-' * len(module.name)))
while not edit and self.weboob.backends_config.backend_exists(name):
print('Backend instance "%s" already exists in "%s"' % (name, self.weboob.backends_config.confpath), file=self.stderr)
if not self.ask('Add new backend for module "%s"?' % module.name, default=False):
return 1
name = self.ask('Please give new instance name', default='%s2' % name, regexp=r'^[\w\-_]+$')
try:
config = config.load(self.weboob, module.name, name, params, nofail=True)
for key, value in params.iteritems():
if key.startswith('_'):
continue
config[key].set(value)
config.save(edit=edit)
print('Backend "%s" successfully %s.' % (name, 'edited' if edit else 'added'))
return name
except BackendAlreadyExists:
print('Backend "%s" already exists.' % name, file=self.stderr)
return 1
def ask(self, question, default=None, masked=None, regexp=None, choices=None, tiny=None):
"""
Ask a question to user.
@param question text displayed (str)
@param default optional default value (str)
@param masked if True, do not show typed text (bool)
@param regexp text must match this regexp (str)
@param choices choices to do (list)
@param tiny ask for the (small) value of the choice (bool)
@return entered text by user (str)
"""
if isinstance(question, Value):
v = copy(question)
if default is not None:
v.default = to_unicode(default) if isinstance(default, str) else default
if masked is not None:
v.masked = masked
if regexp is not None:
v.regexp = regexp
if choices is not None:
v.choices = choices
if tiny is not None:
v.tiny = tiny
else:
if isinstance(default, bool):
klass = ValueBool
elif isinstance(default, float):
klass = ValueFloat
elif isinstance(default, (int,long)):
klass = ValueInt
else:
klass = Value
v = klass(label=question, default=default, masked=masked, regexp=regexp, choices=choices, tiny=tiny)
question = v.label
if v.id:
question = u'[%s] %s' % (v.id, question)
if isinstance(v, ValueBackendPassword):
print(question.encode(self.encoding) + ':')
question = v.label
choices = OrderedDict()
choices['c'] = 'Run an external tool during backend load'
if not v.noprompt:
choices['p'] = 'Prompt value when needed (do not store it)'
choices['s'] = 'Store value in config'
if v.is_command(v.default):
d = 'c'
elif v.default == '' and not v.noprompt:
d = 'p'
else:
d = 's'
r = self.ask('*** How do you want to store it?', choices=choices, tiny=True, default=d)
if r == 'p':
return ''
if r == 'c':
print('Enter the shell command that will print the required value on the standard output')
if v.is_command(v.default):
print(': %s' % v.default[1:-1])
else:
d = None
while True:
cmd = self.ask('')
try:
subprocess.check_output(cmd, shell=True)
except subprocess.CalledProcessError as e:
print('%s' % e)
else:
return '`%s`' % cmd
aliases = {}
if isinstance(v, ValueBool):
question = u'%s (%s/%s)' % (question, 'Y' if v.default else 'y', 'n' if v.default else 'N')
elif v.choices:
if v.tiny is None:
v.tiny = True
for key in v.choices.iterkeys():
if len(key) > 5 or ' ' in key:
v.tiny = False
break
if v.tiny:
question = u'%s (%s)' % (question, '/'.join((s.upper() if s == v.default else s)
for s in v.choices.iterkeys()))
for s in v.choices.iterkeys():
if s == v.default:
aliases[s.upper()] = s
for key, value in v.choices.iteritems():
print(' %s%s%s: %s' % (self.BOLD, key, self.NC, value))
else:
for n, (key, value) in enumerate(v.choices.iteritems()):
print(' %s%2d)%s %s' % (self.BOLD, n + 1, self.NC, value))
aliases[str(n + 1)] = key
question = u'%s (choose in list)' % question
if v.masked:
question = u'%s (hidden input)' % question
if not isinstance(v, ValueBool) and not v.tiny and v.default not in (None, ''):
question = u'%s [%s]' % (question, '*******' if v.masked else v.default)
question += ': '
while True:
if v.masked:
if sys.platform == 'win32':
line = getpass.getpass(str(question))
else:
line = getpass.getpass(question.encode(self.encoding))
else:
self.stdout.write(question.encode(self.encoding))
self.stdout.flush()
line = self.stdin.readline()
if len(line) == 0:
raise EOFError()
else:
line = line.rstrip('\r\n')
if not line and v.default is not None:
line = v.default
if isinstance(line, str):
line = line.decode('utf-8')
if line in aliases:
line = aliases[line]
try:
v.set(line)
except ValueError as e:
print(u'Error: %s' % e, file=self.stderr)
else:
break
v.noprompt = True
return v.get()
def acquire_input(self, content=None, editor_params=None):
editor = os.getenv('EDITOR', 'vi')
if self.stdin.isatty() and editor:
from tempfile import NamedTemporaryFile
with NamedTemporaryFile() as f:
filename = f.name
if content is not None:
if isinstance(content, unicode):
content = content.encode(self.encoding)
f.write(content)
f.flush()
try:
params = editor_params[os.path.basename(editor)]
except (KeyError,TypeError):
params = ''
os.system("%s %s %s" % (editor, params, filename))
f.seek(0)
text = f.read()
else:
if self.stdin.isatty():
print('Reading content from stdin... Type ctrl-D ' \
'from an empty line to stop.')
text = self.stdin.read()
return text.decode(self.encoding)
def bcall_error_handler(self, backend, error, backtrace):
"""
Handler for an exception inside the CallErrors exception.
This method can be overrided to support more exceptions types.
"""
if isinstance(error, BrowserIncorrectPassword):
msg = unicode(error)
if not msg:
msg = 'invalid login/password.'
print('Error(%s): %s' % (backend.name, msg), file=self.stderr)
if self.ask('Do you want to reconfigure this backend?', default=True):
self.unload_backends(names=[backend.name])
self.edit_backend(backend.name)
self.load_backends(names=[backend.name])
elif isinstance(error, BrowserUnavailable):
msg = unicode(error)
if not msg:
msg = 'website is unavailable.'
print(u'Error(%s): %s' % (backend.name, msg), file=self.stderr)
elif isinstance(error, BrowserForbidden):
print(u'Error(%s): %s' % (backend.name, msg or 'Forbidden'), file=self.stderr)
elif isinstance(error, NotImplementedError):
print(u'Error(%s): this feature is not supported yet by this backend.' % backend.name, file=self.stderr)
print(u' %s To help the maintainer of this backend implement this feature,' % (' ' * len(backend.name)), file=self.stderr)
print(u' %s please contact: %s <%s@issues.weboob.org>' % (' ' * len(backend.name), backend.MAINTAINER, backend.NAME), file=self.stderr)
elif isinstance(error, UserError):
print(u'Error(%s): %s' % (backend.name, to_unicode(error)), file=self.stderr)
elif isinstance(error, MoreResultsAvailable):
print(u'Hint: There are more results for backend %s' % (backend.name), file=self.stderr)
elif isinstance(error, BrowserSSLError):
print(u'FATAL(%s): ' % backend.name + self.BOLD + '/!\ SERVER CERTIFICATE IS INVALID /!\\' + self.NC, file=self.stderr)
else:
print(u'Bug(%s): %s' % (backend.name, to_unicode(error)), file=self.stderr)
minfo = self.weboob.repositories.get_module_info(backend.NAME)
if minfo and not minfo.is_local():
self.weboob.repositories.update_repositories()
# minfo of the new available module
minfo = self.weboob.repositories.get_module_info(backend.NAME)
if minfo and minfo.version > self.weboob.repositories.versions.get(minfo.name) and \
self.ask('A new version of %s is available. Do you want to install it?' % minfo.name, default=True) and \
self.install_module(minfo):
print('New version of module %s has been installed. Retry to call the command.' % minfo.name)
return
if logging.root.level == logging.DEBUG:
print(backtrace, file=self.stderr)
else:
return True
def bcall_errors_handler(self, errors, debugmsg='Use --debug option to print backtraces', ignore=()):
"""
Handler for the CallErrors exception.
"""
ask_debug_mode = False
more_results = set()
for backend, error, backtrace in errors.errors:
if isinstance(error, MoreResultsAvailable):
more_results.add(backend.name)
elif isinstance(error, ignore):
continue
elif self.bcall_error_handler(backend, error, backtrace):
ask_debug_mode = True
if ask_debug_mode:
print(debugmsg, file=self.stderr)
elif len(more_results) > 0:
print('Hint: There are more results available for %s (use option -n or count command)' % (', '.join(more_results)), file=self.stderr)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/ 0000775 0000000 0000000 00000000000 12411220504 0027472 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/__init__.py0000664 0000000 0000000 00000000000 12411220504 0031571 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/csv.py 0000664 0000000 0000000 00000002467 12411220504 0030650 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .iformatter import IFormatter
__all__ = ['CSVFormatter']
class CSVFormatter(IFormatter):
def __init__(self, field_separator=u';'):
IFormatter.__init__(self)
self.field_separator = field_separator
self.started = False
def flush(self):
self.started = False
def format_dict(self, item):
result = u''
if not self.started:
result += self.field_separator.join(item.iterkeys()) + '\n'
self.started = True
result += self.field_separator.join(unicode(v) for v in item.itervalues())
return result
iformatter.py 0000664 0000000 0000000 00000021071 12411220504 0032142 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters # -*- coding: utf-8 -*-
# Copyright(C) 2010-2013 Christophe Benz, Julien Hebert
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import sys
import subprocess
if sys.platform == 'win32':
import WConio
try:
from termcolor import colored
except ImportError:
def colored(s, color=None, on_color=None, attrs=None):
if os.getenv('ANSI_COLORS_DISABLED') is None \
and attrs is not None and 'bold' in attrs:
return '%s%s%s' % (IFormatter.BOLD, s, IFormatter.NC)
else:
return s
try:
import tty
import termios
except ImportError:
PROMPT = '--Press return to continue--'
def readch():
return sys.stdin.readline()
else:
PROMPT = '--Press a key to continue--'
def readch():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
tty.setraw(fd)
try:
c = sys.stdin.read(1)
# XXX do not read magic number
if c == '\x03':
raise KeyboardInterrupt()
return c
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
from weboob.capabilities.base import BaseObject
from weboob.tools.application.console import ConsoleApplication
from weboob.tools.ordereddict import OrderedDict
__all__ = ['IFormatter', 'MandatoryFieldsNotFound']
class MandatoryFieldsNotFound(Exception):
def __init__(self, missing_fields):
Exception.__init__(self, u'Mandatory fields not found: %s.' % ', '.join(missing_fields))
class IFormatter(object):
# Tuple of fields mandatory to not crash
MANDATORY_FIELDS = None
# Tuple of displayed field. Set to None if all available fields are
# displayed
DISPLAYED_FIELDS = None
BOLD = ConsoleApplication.BOLD
NC = ConsoleApplication.NC
def colored(self, string, color, attrs=None, on_color=None):
if self.outfile != sys.stdout or not (os.isatty(self.outfile.fileno())):
return string
if isinstance(attrs, basestring):
attrs = [attrs]
return colored(string, color, on_color=on_color, attrs=attrs)
def __init__(self, display_keys=True, display_header=True, outfile=sys.stdout):
self.display_keys = display_keys
self.display_header = display_header
self.interactive = False
self.print_lines = 0
self.termrows = 0
self.outfile = outfile
# XXX if stdin is not a tty, it seems that the command fails.
if os.isatty(sys.stdout.fileno()) and os.isatty(sys.stdin.fileno()):
if sys.platform == 'win32':
self.termrows = WConio.gettextinfo()[8]
else:
self.termrows = int(
subprocess.Popen('stty size', shell=True, stdout=subprocess.PIPE).communicate()[0].split()[0]
)
def output(self, formatted):
if self.outfile != sys.stdout:
with open(self.outfile, "a+") as outfile:
outfile.write(formatted.encode('utf-8') + os.linesep)
else:
for line in formatted.split('\n'):
if self.termrows and (self.print_lines + 1) >= self.termrows:
self.outfile.write(PROMPT)
self.outfile.flush()
readch()
self.outfile.write('\b \b' * len(PROMPT))
self.print_lines = 0
if isinstance(line, unicode):
line = line.encode('utf-8')
print(line)
self.print_lines += 1
def start_format(self, **kwargs):
pass
def flush(self):
pass
def format(self, obj, selected_fields=None, alias=None):
"""
Format an object to be human-readable.
An object has fields which can be selected.
:param obj: object to format
:type obj: BaseObject or dict
:param selected_fields: fields to display. If None, all fields are selected
:type selected_fields: tuple
:param alias: an alias to use instead of the object's ID
:type alias: unicode
"""
if isinstance(obj, BaseObject):
if selected_fields: # can be an empty list (nothing to do), or None (return all fields)
obj = obj.copy()
for name, value in obj.iter_fields():
if not name in selected_fields:
delattr(obj, name)
if self.MANDATORY_FIELDS:
missing_fields = set(self.MANDATORY_FIELDS) - set([name for name, value in obj.iter_fields()])
if missing_fields:
raise MandatoryFieldsNotFound(missing_fields)
formatted = self.format_obj(obj, alias)
else:
try:
obj = OrderedDict(obj)
except ValueError:
raise TypeError('Please give a BaseObject or a dict')
if selected_fields:
obj = obj.copy()
for name, value in obj.iteritems():
if not name in selected_fields:
obj.pop(name)
if self.MANDATORY_FIELDS:
missing_fields = set(self.MANDATORY_FIELDS) - set(obj.iterkeys())
if missing_fields:
raise MandatoryFieldsNotFound(missing_fields)
formatted = self.format_dict(obj)
if formatted:
self.output(formatted)
return formatted
def format_obj(self, obj, alias=None):
"""
Format an object to be human-readable.
Called by format().
This method has to be overridden in child classes.
:param obj: object to format
:type obj: BaseObject
:rtype: str
"""
return self.format_dict(obj.to_dict())
def format_dict(self, obj):
"""
Format a dict to be human-readable.
:param obj: dict to format
:type obj: dict
:rtype: str
"""
return NotImplementedError()
def format_collection(self, collection, only):
"""
Format a collection to be human-readable.
:param collection: collection to format
:type collection: BaseCollection
:rtype: str
"""
if only is False or collection.basename in only:
if collection.basename and collection.title:
self.output(u'%s~ (%s) %s (%s)%s' %
(self.BOLD, collection.basename, collection.title, collection.backend, self.NC))
else:
self.output(u'%s~ (%s) (%s)%s' %
(self.BOLD, collection.basename, collection.backend, self.NC))
class PrettyFormatter(IFormatter):
def format_obj(self, obj, alias):
title = self.get_title(obj)
desc = self.get_description(obj)
if alias is not None:
result = u'%s %s %s (%s)' % (self.colored('%2s' % alias, 'red', 'bold'),
self.colored(u'—', 'cyan', 'bold'),
self.colored(title, 'yellow', 'bold'),
self.colored(obj.backend, 'blue', 'bold'))
else:
result = u'%s %s %s' % (self.colored(obj.fullid, 'red', 'bold'),
self.colored(u'—', 'cyan', 'bold'),
self.colored(title, 'yellow', 'bold'))
if desc is not None:
result += u'%s\t%s' % (os.linesep, self.colored(desc, 'white'))
return result
def get_title(self, obj):
raise NotImplementedError()
def get_description(self, obj):
return None
def formatter_test_output(Formatter, obj):
"""
Formats an object and returns output as a string.
For test purposes only.
"""
from tempfile import mkstemp
from os import remove
_, name = mkstemp()
fmt = Formatter()
fmt.outfile = name
fmt.format(obj)
fmt.flush()
with open(name) as f:
res = f.read()
remove(name)
return res
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/json.py 0000664 0000000 0000000 00000004316 12411220504 0031021 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2013-2014 Julien Hebert, Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.base import NotAvailable, NotLoaded
from weboob.tools.json import json
from .iformatter import IFormatter
__all__ = ['JsonFormatter', 'JsonLineFormatter']
class Encoder(json.JSONEncoder):
"generic weboob object encoder"
def default(self, obj):
try:
return json.JSONEncoder.default(self, obj)
except TypeError:
if obj is NotAvailable or obj is NotLoaded:
return None
try:
dct = obj.to_dict()
except AttributeError:
return str(obj)
return dct
class JsonFormatter(IFormatter):
"""
Formats the whole list as a single JSON list object.
"""
def __init__(self):
IFormatter.__init__(self)
self.queue = []
def flush(self):
self.output(json.dumps(self.queue, cls=Encoder))
def format_dict(self, item):
self.queue.append(item)
def format_collection(self, collection, only):
self.queue.append(collection.to_dict())
class JsonLineFormatter(IFormatter):
"""
Formats the list as received, with a JSON object per line.
The advantage is that it can be streamed.
"""
def format_dict(self, item):
self.output(json.dumps(item, cls=Encoder))
def test():
from .iformatter import formatter_test_output as fmt
assert fmt(JsonFormatter, {'foo': 'bar'}) == '[{"foo": "bar"}]\n'
assert fmt(JsonLineFormatter, {'foo': 'bar'}) == '{"foo": "bar"}\n'
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/load.py 0000664 0000000 0000000 00000005302 12411220504 0030763 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz, Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
__all__ = ['FormattersLoader', 'FormatterLoadError']
class FormatterLoadError(Exception):
pass
class FormattersLoader(object):
BUILTINS = ['htmltable', 'multiline', 'simple', 'table', 'csv', 'webkit', 'json', 'json_line']
def __init__(self):
self.formatters = {}
def register_formatter(self, name, klass):
self.formatters[name] = klass
def get_available_formatters(self):
l = set(self.formatters.iterkeys())
l = l.union(self.BUILTINS)
l = sorted(l)
return l
def build_formatter(self, name):
if name not in self.formatters:
try:
self.formatters[name] = self.load_builtin_formatter(name)
except ImportError as e:
FormattersLoader.BUILTINS.remove(name)
raise FormatterLoadError('Unable to load formatter "%s": %s' % (name, e))
return self.formatters[name]()
def load_builtin_formatter(self, name):
if name not in self.BUILTINS:
raise FormatterLoadError('Formatter "%s" does not exist' % name)
if name == 'htmltable':
from .table import HTMLTableFormatter
return HTMLTableFormatter
elif name == 'table':
from .table import TableFormatter
return TableFormatter
elif name == 'simple':
from .simple import SimpleFormatter
return SimpleFormatter
elif name == 'multiline':
from .multiline import MultilineFormatter
return MultilineFormatter
elif name == 'webkit':
from .webkit import WebkitGtkFormatter
return WebkitGtkFormatter
elif name == 'csv':
from .csv import CSVFormatter
return CSVFormatter
elif name == 'json':
from .json import JsonFormatter
return JsonFormatter
elif name == 'json_line':
from .json import JsonLineFormatter
return JsonLineFormatter
multiline.py 0000664 0000000 0000000 00000002706 12411220504 0031774 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities.base import NotLoaded, NotAvailable
from .iformatter import IFormatter
__all__ = ['MultilineFormatter']
class MultilineFormatter(IFormatter):
def __init__(self, key_value_separator=u': ', after_item=u'\n'):
IFormatter.__init__(self)
self.key_value_separator = key_value_separator
self.after_item = after_item
def flush(self):
pass
def format_dict(self, item):
result = u'\n'.join(u'%s%s' % (
(u'%s%s' % (k, self.key_value_separator) if self.display_keys else ''), v)
for k, v in item.iteritems() if (v is not NotLoaded and v is not NotAvailable))
if len(item) > 1:
result += self.after_item
return result
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/simple.py 0000664 0000000 0000000 00000002363 12411220504 0031341 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .iformatter import IFormatter
__all__ = ['SimpleFormatter']
class SimpleFormatter(IFormatter):
def __init__(self, field_separator=u'\t', key_value_separator=u'='):
IFormatter.__init__(self)
self.field_separator = field_separator
self.key_value_separator = key_value_separator
def format_dict(self, item):
return self.field_separator.join(u'%s%s' % (
(u'%s%s' % (k, self.key_value_separator) if self.display_keys else ''), v)
for k, v in item.iteritems())
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/table.py 0000664 0000000 0000000 00000006346 12411220504 0031144 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from prettytable import PrettyTable
from weboob.capabilities.base import empty
from .iformatter import IFormatter
__all__ = ['TableFormatter', 'HTMLTableFormatter']
class TableFormatter(IFormatter):
HTML = False
def __init__(self):
IFormatter.__init__(self)
self.queue = []
self.keys = None
self.header = None
def flush(self):
s = self.get_formatted_table()
if s is not None:
self.output(s.encode('utf-8'))
def get_formatted_table(self):
if len(self.queue) == 0:
return
queue = [() for i in xrange(len(self.queue))]
column_headers = []
# Do not display columns when all values are NotLoaded or NotAvailable
for i in xrange(len(self.keys)):
available = False
for line in self.queue:
if not empty(line[i]):
available = True
break
if available:
column_headers.append(self.keys[i].capitalize().replace('_', ' '))
for j in xrange(len(self.queue)):
queue[j] += (self.queue[j][i],)
s = ''
if self.display_header and self.header:
if self.HTML:
s += '
%s
' % self.header
else:
s += self.header
s += "\n"
table = PrettyTable(list(column_headers))
for column_header in column_headers:
# API changed in python-prettytable. The try/except is a bad hack to support both versions
# Note: two versions are not exactly the same...
# (first one: header in center. Second one: left align for header too)
try:
table.set_field_align(column_header, 'l')
except:
table.align[column_header] = 'l'
for line in queue:
table.add_row(line)
if self.HTML:
s += table.get_html_string()
else:
s += table.get_string()
self.queue = []
return s
def format_dict(self, item):
if self.keys is None:
self.keys = item.keys()
self.queue.append(item.values())
def set_header(self, string):
self.header = string
class HTMLTableFormatter(TableFormatter):
HTML = True
def test():
from .iformatter import formatter_test_output as fmt
assert fmt(TableFormatter, {'foo': 'bar'}) == \
'+-----+\n' \
'| Foo |\n' \
'+-----+\n' \
'| bar |\n' \
'+-----+\n'
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/webkit/ 0000775 0000000 0000000 00000000000 12411220504 0030757 5 ustar 00root root 0000000 0000000 __init__.py 0000664 0000000 0000000 00000001456 12411220504 0033017 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/webkit # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from .webkitgtk import WebkitGtkFormatter
__all__ = ['WebkitGtkFormatter']
webkitgtk.py 0000664 0000000 0000000 00000005133 12411220504 0033247 0 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/formatters/webkit # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
import gtk
import webkit
from weboob.tools.application.javascript import get_javascript
from ..table import HTMLTableFormatter
__all__ = ['WebkitGtkFormatter']
class WebBrowser(gtk.Window):
def __init__(self):
gtk.Window.__init__(self)
self.connect('destroy', gtk.main_quit)
self.set_default_size(800, 600)
self.web_view = webkit.WebView()
sw = gtk.ScrolledWindow()
sw.add(self.web_view)
self.add(sw)
self.show_all()
class WebkitGtkFormatter(HTMLTableFormatter):
def flush(self):
table_string = self.get_formatted_table()
js_filepaths = []
js_filepaths.append(get_javascript('jquery'))
js_filepaths.append(get_javascript('tablesorter'))
scripts = ['' % js_filepath for js_filepath in js_filepaths]
html_string_params = dict(table=table_string)
if scripts:
html_string_params['scripts'] = ''.join(scripts)
html_string = """
%(scripts)s
%(table)s
""" % html_string_params
web_browser = WebBrowser()
web_browser.web_view.load_html_string(html_string, 'file://%s' % os.path.abspath(os.getcwd()))
gtk.main()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/javascript.py 0000664 0000000 0000000 00000003444 12411220504 0030031 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
__all__ = ['get_javascript']
def get_javascript(name, load_order=('local', 'web'), minified=True):
if name == 'jquery':
for src in load_order:
if src == 'local':
# try Debian paths
if minified:
filepath = '/usr/share/javascript/jquery/jquery.min.js'
else:
filepath = '/usr/share/javascript/jquery/jquery.js'
if os.path.exists(filepath):
return filepath
elif src == 'web':
# return Google-hosted URLs
if minified:
return 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js'
else:
return 'http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js'
elif name == 'tablesorter':
if 'web' in load_order:
if minified:
return 'http://tablesorter.com/jquery.tablesorter.min.js'
else:
return 'http://tablesorter.com/jquery.tablesorter.js'
return None
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/media_player.py 0000664 0000000 0000000 00000015601 12411220504 0030314 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz, Romain Bignon, John Obbele
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
from subprocess import PIPE, Popen
import cookielib
import urllib2
from weboob.tools.log import getLogger
__all__ = ['InvalidMediaPlayer', 'MediaPlayer', 'MediaPlayerNotFound']
PLAYERS = (
('mpv', '-'),
('mplayer2', '-'),
('mplayer', '-'),
('vlc', '-'),
('parole', 'fd://0'),
('totem', 'fd://0'),
('xine', 'stdin:/'),
)
class MediaPlayerNotFound(Exception):
def __init__(self):
Exception.__init__(self, u'No media player found on this system. Please install one of them: %s.' %
', '.join(player[0] for player in PLAYERS))
class InvalidMediaPlayer(Exception):
def __init__(self, player_name):
Exception.__init__(self, u'Invalid media player: %s. Valid media players: %s.' % (
player_name, ', '.join(player[0] for player in PLAYERS)))
class MediaPlayer(object):
"""
Black magic invoking a media player to this world.
Presently, due to strong disturbances in the holidays of the ether
world, the media player used is chosen from a static list of
programs. See PLAYERS for more information.
"""
def __init__(self, logger=None):
self.logger = getLogger('mediaplayer', logger)
def guess_player_name(self):
for player_name in [player[0] for player in PLAYERS]:
if self._find_in_path(os.environ['PATH'], player_name):
return player_name
return None
def play(self, media, player_name=None, player_args=None):
"""
Play a media object, using programs from the PLAYERS list.
This function dispatch calls to either _play_default or
_play_rtmp for special rtmp streams using SWF verification.
"""
player_names = [player[0] for player in PLAYERS]
if not player_name:
self.logger.debug(u'No media player given. Using the first available from: %s.' %
', '.join(player_names))
player_name = self.guess_player_name()
if player_name is None:
raise MediaPlayerNotFound()
if media.url.startswith('rtmp'):
self._play_rtmp(media, player_name, args=player_args)
else:
self._play_default(media, player_name, args=player_args)
def _play_default(self, media, player_name, args=None):
"""
Play media.url with the media player.
"""
# if flag play_proxy...
if hasattr(media, '_play_proxy') and media._play_proxy == True:
# use urllib2 to handle redirect and cookies
self._play_proxy(media, player_name, args)
return None
args = player_name.split(' ')
player_name = args[0]
args.append(media.url)
print('Invoking "%s".' % (' '.join(args)))
os.spawnlp(os.P_WAIT, player_name, *args)
def _play_proxy(self, media, player_name, args):
"""
Load data with python urllib2 and pipe data to a media player.
We need this function for url that use redirection and cookies.
This function is used if the non-standard,
non-API compliant '_play_proxy' attribute of the 'media' object is defined and is True.
"""
if args is None:
for (binary, stdin_args) in PLAYERS:
if binary == player_name:
args = stdin_args
assert args is not None
print(':: Play_proxy streaming from %s' % media.url)
print(':: to %s %s' % (player_name, args))
print(player_name + ' ' + args)
proc = Popen(player_name + ' ' + args, stdin=PIPE, shell=True)
# Handle cookies (and redirection 302...)
cj = cookielib.CookieJar()
url_opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
url_handler = url_opener.open(media.url)
file_size = int(url_handler.info().getheaders("Content-Length")[0])
file_size_dl = 0
block_sz = 8192
while file_size_dl < file_size:
_buffer = url_handler.read(block_sz)
if not buffer:
break
file_size_dl += len(_buffer)
try:
proc.stdin.write(_buffer)
except:
print("play_proxy broken pipe. Can't write anymore.")
break
def _play_rtmp(self, media, player_name, args):
"""
Download data with rtmpdump and pipe them to a media player.
You need a working version of rtmpdump installed and the SWF
object url in order to comply with SWF verification requests
from the server. The last one is retrieved from the non-standard
non-API compliant 'swf_player' attribute of the 'media' object.
"""
if not self._find_in_path(os.environ['PATH'], 'rtmpdump'):
self.logger.warning('"rtmpdump" binary not found')
return self._play_default(media, player_name)
media_url = media.url
try:
player_url = media.swf_player
if media.swf_player:
rtmp = 'rtmpdump -r %s --swfVfy %s' % (media_url, player_url)
else:
rtmp = 'rtmpdump -r %s' % media_url
except AttributeError:
self.logger.warning('Your media object does not have a "swf_player" attribute. SWF verification will be '
'disabled and may prevent correct media playback.')
return self._play_default(media, player_name)
rtmp += ' --quiet'
if args is None:
for (binary, stdin_args) in PLAYERS:
if binary == player_name:
args = stdin_args
assert args is not None
player_name = player_name.split(' ')
args = args.split(' ')
print(':: Streaming from %s' % media_url)
print(':: to %s %s' % (player_name, args))
print(':: %s' % rtmp)
p1 = Popen(rtmp.split(), stdout=PIPE)
Popen(player_name + args, stdin=p1.stdout, stderr=PIPE)
def _find_in_path(self, path, filename):
for i in path.split(':'):
if os.path.exists('/'.join([i, filename])):
return True
return False
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/qt/ 0000775 0000000 0000000 00000000000 12411220504 0025730 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/qt/Makefile 0000664 0000000 0000000 00000000264 12411220504 0027372 0 ustar 00root root 0000000 0000000 UI_FILES = $(wildcard *.ui)
UI_PY_FILES = $(UI_FILES:%.ui=%_ui.py)
PYUIC = pyuic4
all: $(UI_PY_FILES)
%_ui.py: %.ui
$(PYUIC) -o $@ $^
clean:
rm -f *.pyc
rm -f $(UI_PY_FILES)
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/qt/__init__.py 0000664 0000000 0000000 00000000301 12411220504 0030033 0 ustar 00root root 0000000 0000000 from .qt import QtApplication, QtMainWindow, QtDo, HTMLDelegate
from .backendcfg import BackendCfg
__all__ = ['QtApplication', 'QtMainWindow', 'QtDo', 'HTMLDelegate',
'BackendCfg']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/qt/backendcfg.py 0000664 0000000 0000000 00000050000 12411220504 0030344 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from PyQt4.QtGui import QDialog, QTreeWidgetItem, QLabel, QFormLayout, \
QMessageBox, QPixmap, QImage, QIcon, QHeaderView, \
QListWidgetItem, QTextDocument, QVBoxLayout, \
QDialogButtonBox, QProgressDialog
from PyQt4.QtCore import SIGNAL, Qt, QVariant, QUrl, QThread
import re
import os
from logging import warning
from weboob.core.modules import ModuleLoadError
from weboob.core.repositories import IProgress, ModuleInstallError
from weboob.core.backendscfg import BackendAlreadyExists
from weboob.capabilities.account import CapAccount, Account, AccountRegisterError
from weboob.tools.application.qt.backendcfg_ui import Ui_BackendCfg
from weboob.tools.application.qt.reposdlg_ui import Ui_RepositoriesDlg
from weboob.tools.ordereddict import OrderedDict
from weboob.tools.misc import to_unicode
from .qt import QtValue
class RepositoriesDialog(QDialog):
def __init__(self, filename, parent=None):
QDialog.__init__(self, parent)
self.filename = filename
self.ui = Ui_RepositoriesDlg()
self.ui.setupUi(self)
self.connect(self.ui.buttonBox, SIGNAL('accepted()'), self.save)
with open(self.filename, 'r') as fp:
self.ui.reposEdit.setPlainText(fp.read())
def save(self):
with open(self.filename, 'w') as fp:
fp.write(self.ui.reposEdit.toPlainText())
self.accept()
class IconFetcher(QThread):
def __init__(self, weboob, item, minfo):
QThread.__init__(self)
self.weboob = weboob
self.items = [item]
self.minfo = minfo
def run(self):
self.weboob.repositories.retrieve_icon(self.minfo)
self.emit(SIGNAL('retrieved'), self)
class ProgressDialog(IProgress, QProgressDialog):
def __init__(self, *args, **kwargs):
QProgressDialog.__init__(self, *args, **kwargs)
def progress(self, percent, message):
self.setValue(int(percent * 100))
self.setLabelText(message)
def error(self, message):
QMessageBox.critical(self, self.tr('Error'), '%s' % message, QMessageBox.Ok)
class BackendCfg(QDialog):
def __init__(self, weboob, caps=None, parent=None):
QDialog.__init__(self, parent)
self.ui = Ui_BackendCfg()
self.ui.setupUi(self)
self.ui.backendsList.sortByColumn(0, Qt.AscendingOrder)
self.to_unload = set()
self.to_load = set()
self.weboob = weboob
self.caps = caps
self.config_widgets = {}
# This attribute is set when itemChanged it called, because when
# a backend is enabled/disabled, we don't want to display its config
# frame, and the itemClicked event is always emit just after a
# itemChanged event.
# is_enabling is a counter to prevent race conditions.
self.is_enabling = 0
self.ui.backendsList.header().setResizeMode(QHeaderView.ResizeToContents)
self.ui.configFrame.hide()
self.icon_cache = {}
self.icon_threads = {}
self.loadModules()
self.loadBackendsList()
self.connect(self.ui.updateButton, SIGNAL('clicked()'), self.updateModules)
self.connect(self.ui.repositoriesButton, SIGNAL('clicked()'), self.editRepositories)
self.connect(self.ui.backendsList, SIGNAL('itemClicked(QTreeWidgetItem *, int)'),
self.backendClicked)
self.connect(self.ui.backendsList, SIGNAL('itemChanged(QTreeWidgetItem *, int)'),
self.backendEnabled)
self.connect(self.ui.modulesList, SIGNAL('itemSelectionChanged()'), self.moduleSelectionChanged)
self.connect(self.ui.proxyBox, SIGNAL('toggled(bool)'), self.proxyEditEnabled)
self.connect(self.ui.addButton, SIGNAL('clicked()'), self.addEvent)
self.connect(self.ui.removeButton, SIGNAL('clicked()'), self.removeEvent)
self.connect(self.ui.registerButton, SIGNAL('clicked()'), self.registerEvent)
self.connect(self.ui.configButtonBox, SIGNAL('accepted()'), self.acceptBackend)
self.connect(self.ui.configButtonBox, SIGNAL('rejected()'), self.rejectBackend)
def get_icon_cache(self, path):
if not path in self.icon_cache:
img = QImage(path)
self.icon_cache[path] = QIcon(QPixmap.fromImage(img))
return self.icon_cache[path]
def set_icon(self, item, minfo):
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
icon = self.icon_cache.get(icon_path, None)
if icon is None and not os.path.exists(icon_path):
if minfo.name in self.icon_threads:
self.icon_threads[minfo.name].items.append(item)
else:
thread = IconFetcher(self.weboob, item, minfo)
self.connect(thread, SIGNAL('retrieved'), lambda t: self._set_icon(t.items, t.minfo))
self.icon_threads[minfo.name] = thread
thread.start()
return
self._set_icon([item], minfo)
def _set_icon(self, items, minfo):
icon_path = self.weboob.repositories.get_module_icon_path(minfo)
icon = self.get_icon_cache(icon_path)
if icon is None:
return
for item in items:
try:
item.setIcon(icon)
except TypeError:
item.setIcon(0, icon)
self.icon_threads.pop(minfo.name, None)
def updateModules(self):
self.ui.configFrame.hide()
pd = ProgressDialog('Update of modules', "Cancel", 0, 100, self)
pd.setWindowModality(Qt.WindowModal)
try:
self.weboob.repositories.update(pd)
except ModuleInstallError as err:
QMessageBox.critical(self, self.tr('Update error'),
unicode(self.tr('Unable to update modules: %s' % (err))),
QMessageBox.Ok)
pd.setValue(100)
self.loadModules()
QMessageBox.information(self, self.tr('Update of modules'),
self.tr('Modules updated!'), QMessageBox.Ok)
def editRepositories(self):
if RepositoriesDialog(self.weboob.repositories.sources_list).exec_():
self.updateModules()
def loadModules(self):
self.ui.modulesList.clear()
for name, module in sorted(self.weboob.repositories.get_all_modules_info(self.caps).iteritems()):
item = QListWidgetItem(name.capitalize())
self.set_icon(item, module)
self.ui.modulesList.addItem(item)
def askInstallModule(self, minfo):
reply = QMessageBox.question(self, self.tr('Install a module'),
unicode(self.tr("Module %s is not installed. Do you want to install it?")) % minfo.name,
QMessageBox.Yes|QMessageBox.No)
if reply != QMessageBox.Yes:
return False
return self.installModule(minfo)
def installModule(self, minfo):
pd = ProgressDialog('Installation of %s' % minfo.name, "Cancel", 0, 100, self)
pd.setWindowModality(Qt.WindowModal)
try:
self.weboob.repositories.install(minfo, pd)
except ModuleInstallError as err:
QMessageBox.critical(self, self.tr('Install error'),
unicode(self.tr('Unable to install module %s: %s' % (minfo.name, err))),
QMessageBox.Ok)
pd.setValue(100)
return True
def loadBackendsList(self):
self.ui.backendsList.clear()
for instance_name, name, params in self.weboob.backends_config.iter_backends():
info = self.weboob.repositories.get_module_info(name)
if not info or (self.caps and not info.has_caps(self.caps)):
continue
item = QTreeWidgetItem(None, [instance_name, name])
item.setCheckState(0, Qt.Checked if params.get('_enabled', '1').lower() in ('1', 'y', 'true')
else Qt.Unchecked)
self.set_icon(item, info)
self.ui.backendsList.addTopLevelItem(item)
def backendEnabled(self, item, col):
self.is_enabling += 1
instname = unicode(item.text(0))
bname = unicode(item.text(1))
if item.checkState(0) == Qt.Checked:
self.to_load.add(instname)
enabled = '1'
else:
self.to_unload.add(instname)
try:
self.to_load.remove(instname)
except KeyError:
pass
enabled = '0'
self.weboob.backends_config.edit_backend(instname, bname, {'_enabled': enabled})
def backendClicked(self, item, col):
if self.is_enabling:
self.is_enabling -= 1
return
bname = unicode(item.text(0))
self.editBackend(bname)
def addEvent(self):
self.editBackend()
def removeEvent(self):
item = self.ui.backendsList.currentItem()
if not item:
return
bname = unicode(item.text(0))
reply = QMessageBox.question(self, self.tr('Remove a backend'),
unicode(self.tr("Are you sure you want to remove the backend '%s'?")) % bname,
QMessageBox.Yes|QMessageBox.No)
if reply != QMessageBox.Yes:
return
self.weboob.backends_config.remove_backend(bname)
self.to_unload.add(bname)
try:
self.to_load.remove(bname)
except KeyError:
pass
self.ui.configFrame.hide()
self.loadBackendsList()
def editBackend(self, name=None):
self.ui.registerButton.hide()
self.ui.configFrame.show()
if name is not None:
bname, params = self.weboob.backends_config.get_backend(name)
items = self.ui.modulesList.findItems(bname, Qt.MatchFixedString)
if not items:
warning('Backend not found')
else:
self.ui.modulesList.setCurrentItem(items[0])
self.ui.modulesList.setEnabled(False)
self.ui.nameEdit.setText(name)
self.ui.nameEdit.setEnabled(False)
if '_proxy' in params:
self.ui.proxyBox.setChecked(True)
self.ui.proxyEdit.setText(params.pop('_proxy'))
else:
self.ui.proxyBox.setChecked(False)
self.ui.proxyEdit.clear()
params.pop('_enabled', None)
info = self.weboob.repositories.get_module_info(bname)
if info and (info.is_installed() or self.installModule(info)):
module = self.weboob.modules_loader.get_or_load_module(bname)
for key, value in module.config.load(self.weboob, bname, name, params, nofail=True).iteritems():
try:
l, widget = self.config_widgets[key]
except KeyError:
warning('Key "%s" is not found' % key)
else:
# Do not prompt user for value (for example a password if it is empty).
value.noprompt = True
widget.set_value(value)
return
self.ui.nameEdit.clear()
self.ui.nameEdit.setEnabled(True)
self.ui.proxyBox.setChecked(False)
self.ui.proxyEdit.clear()
self.ui.modulesList.setEnabled(True)
self.ui.modulesList.setCurrentRow(-1)
def moduleSelectionChanged(self):
for key, (label, value) in self.config_widgets.iteritems():
label.hide()
value.hide()
self.ui.configLayout.removeWidget(label)
self.ui.configLayout.removeWidget(value)
label.deleteLater()
value.deleteLater()
self.config_widgets = {}
self.ui.moduleInfo.clear()
selection = self.ui.modulesList.selectedItems()
if not selection:
return
minfo = self.weboob.repositories.get_module_info(unicode(selection[0].text()).lower())
if not minfo:
warning('Module not found')
return
if not minfo.is_installed() and not self.installModule(minfo):
self.editBackend(None)
return
module = self.weboob.modules_loader.get_or_load_module(minfo.name)
icon_path = os.path.join(self.weboob.repositories.icons_dir, '%s.png' % minfo.name)
img = QImage(icon_path)
self.ui.moduleInfo.document().addResource(QTextDocument.ImageResource, QUrl('mydata://logo.png'),
QVariant(img))
if not module.name in [n for n, ign, ign2 in self.weboob.backends_config.iter_backends()]:
self.ui.nameEdit.setText(module.name)
else:
self.ui.nameEdit.setText('')
self.ui.moduleInfo.setText(to_unicode(self.tr(
u'
%s Module %s
'
'Version: %s '
'Maintainer: %s '
'License: %s '
'%s'
'Description: %s '
'Capabilities: %s '))
% ('',
module.name.capitalize(),
module.version,
to_unicode(module.maintainer).replace(u'&', u'&').replace(u'<', u'<').replace(u'>', u'>'),
module.license,
(unicode(self.tr('Website: %s ')) % module.website) if module.website else '',
module.description,
', '.join(sorted(cap.__name__.replace('Cap', '') for cap in module.iter_caps()))))
if module.has_caps(CapAccount) and self.ui.nameEdit.isEnabled() and \
module.klass.ACCOUNT_REGISTER_PROPERTIES is not None:
self.ui.registerButton.show()
else:
self.ui.registerButton.hide()
for key, field in module.config.iteritems():
label = QLabel(u'%s:' % field.label)
qvalue = QtValue(field)
self.ui.configLayout.addRow(label, qvalue)
self.config_widgets[key] = (label, qvalue)
def proxyEditEnabled(self, state):
self.ui.proxyEdit.setEnabled(state)
def acceptBackend(self):
bname = unicode(self.ui.nameEdit.text())
selection = self.ui.modulesList.selectedItems()
if not selection:
QMessageBox.critical(self, self.tr('Unable to add a backend'),
self.tr('Please select a module'))
return
try:
module = self.weboob.modules_loader.get_or_load_module(unicode(selection[0].text()).lower())
except ModuleLoadError:
module = None
if not module:
QMessageBox.critical(self, self.tr('Unable to add a backend'),
self.tr('The selected module does not exist.'))
return
params = {}
if not bname:
QMessageBox.critical(self, self.tr('Missing field'), self.tr('Please specify a backend name'))
return
if self.ui.nameEdit.isEnabled():
if not re.match(r'^[\w\-_]+$', bname):
QMessageBox.critical(self, self.tr('Invalid value'),
self.tr('The backend name can only contain letters and digits'))
return
if self.weboob.backends_config.backend_exists(bname):
QMessageBox.critical(self, self.tr('Unable to create backend'),
unicode(self.tr('Unable to create backend "%s": it already exists')) % bname)
return
if self.ui.proxyBox.isChecked():
params['_proxy'] = unicode(self.ui.proxyEdit.text())
if not params['_proxy']:
QMessageBox.critical(self, self.tr('Missing field'), self.tr('Please specify a proxy URL'))
return
config = module.config.load(self.weboob, module.name, bname, {}, nofail=True)
for key, field in config.iteritems():
label, qtvalue = self.config_widgets[key]
try:
value = qtvalue.get_value()
except ValueError as e:
QMessageBox.critical(self, self.tr('Invalid value'),
unicode(self.tr('Invalid value for field "%s":
%s')) % (field.label, e))
return
field.set(value.get())
try:
config.save(edit=not self.ui.nameEdit.isEnabled(), params=params)
except BackendAlreadyExists:
QMessageBox.critical(self, self.tr('Unable to create backend'),
unicode(self.tr('Unable to create backend "%s": it already exists')) % bname)
return
self.to_load.add(bname)
self.ui.configFrame.hide()
self.loadBackendsList()
def rejectBackend(self):
self.ui.configFrame.hide()
def registerEvent(self):
selection = self.ui.modulesList.selectedItems()
if not selection:
return
try:
module = self.weboob.modules_loader.get_or_load_module(unicode(selection[0].text()).lower())
except ModuleLoadError:
module = None
if not module:
return
dialog = QDialog(self)
vbox = QVBoxLayout(dialog)
if module.website:
website = 'on the website %s' % module.website
else:
website = 'with the module %s' % module.name
vbox.addWidget(QLabel('To create an account %s, please provide this information:' % website))
formlayout = QFormLayout()
props_widgets = OrderedDict()
for key, prop in module.klass.ACCOUNT_REGISTER_PROPERTIES.iteritems():
widget = QtValue(prop)
formlayout.addRow(QLabel(u'%s:' % prop.label), widget)
props_widgets[prop.id] = widget
vbox.addLayout(formlayout)
buttonBox = QDialogButtonBox(dialog)
buttonBox.setStandardButtons(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
self.connect(buttonBox, SIGNAL("accepted()"), dialog.accept)
self.connect(buttonBox, SIGNAL("rejected()"), dialog.reject)
vbox.addWidget(buttonBox)
end = False
while not end:
end = True
if dialog.exec_():
account = Account()
account.properties = {}
for key, widget in props_widgets.iteritems():
try:
v = widget.get_value()
except ValueError as e:
QMessageBox.critical(self, self.tr('Invalid value'),
unicode(self.tr('Invalid value for field "%s":
%s')) % (key, e))
end = False
break
else:
account.properties[key] = v
if end:
try:
module.klass.register_account(account)
except AccountRegisterError as e:
QMessageBox.critical(self, self.tr('Error during register'),
unicode(self.tr('Unable to register account %s:
%s')) % (website, e))
end = False
else:
for key, value in account.properties.iteritems():
if key in self.config_widgets:
self.config_widgets[key][1].set_value(value)
def run(self):
self.exec_()
ret = (len(self.to_load) > 0 or len(self.to_unload) > 0)
self.weboob.unload_backends(self.to_unload)
self.weboob.load_backends(names=self.to_load)
return ret
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/qt/backendcfg.ui 0000664 0000000 0000000 00000022764 12411220504 0030351 0 ustar 00root root 0000000 0000000
BackendCfg00622516Backends configuration64QFrame::StyledPanelQFrame::Raised4Update modulesRepositoriesQt::Horizontal4020Qt::VerticalQAbstractItemView::NoEditTriggers2424falsefalsefalsetruetruefalsefalsefalsetrueNameModuleQFrame::StyledPanelQFrame::RaisedAddRemoveQt::Vertical2040QFrame::StyledPanelQFrame::RaisedAvailable modules:0024241true10QFrame::NoFrameQFrame::Plain00trueQFormLayout::ExpandingFieldsGrowProxy:falseRegister an account...Name:Qt::HorizontalQDialogButtonBox::Cancel|QDialogButtonBox::OkQDialogButtonBox::ClosebackendsListaddButtonremoveButtonmodulesListmoduleInfonameEditproxyBoxproxyEditregisterButtonconfigButtonBoxbuttonBoxbuttonBoxclicked(QAbstractButton*)BackendCfgaccept()312591312306
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/qt/qt.py 0000664 0000000 0000000 00000031516 12411220504 0026734 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import sys
import logging
import re
from threading import Event
from copy import copy
from PyQt4.QtCore import QTimer, SIGNAL, QObject, QString, QSize, QVariant, QMutex, Qt
from PyQt4.QtGui import QMainWindow, QApplication, QStyledItemDelegate, \
QStyleOptionViewItemV4, QTextDocument, QStyle, \
QAbstractTextDocumentLayout, QPalette, QMessageBox, \
QSpinBox, QLineEdit, QComboBox, QCheckBox, QInputDialog
from weboob.core.ouiboube import Weboob, VersionsMismatchError
from weboob.core.scheduler import IScheduler
from weboob.core.repositories import ModuleInstallError
from weboob.tools.config.iconfig import ConfigError
from weboob.tools.exceptions import BrowserUnavailable, BrowserIncorrectPassword, BrowserForbidden
from weboob.tools.value import ValueInt, ValueBool, ValueBackendPassword
from weboob.tools.misc import to_unicode
from weboob.capabilities import UserError
from ..base import Application, MoreResultsAvailable
__all__ = ['QtApplication', 'QtMainWindow', 'QtDo', 'HTMLDelegate']
class QtScheduler(IScheduler):
def __init__(self, app):
self.app = app
self.count = 0
self.timers = {}
def schedule(self, interval, function, *args):
timer = QTimer()
timer.setInterval(interval * 1000)
timer.setSingleShot(True)
count = self.count
self.count += 1
timer.start()
self.app.connect(timer, SIGNAL("timeout()"), lambda: self.timeout(count, None, function, *args))
self.timers[count] = timer
def repeat(self, interval, function, *args):
timer = QTimer()
timer.setSingleShot(False)
count = self.count
self.count += 1
timer.start(0)
self.app.connect(timer, SIGNAL("timeout()"), lambda: self.timeout(count, interval, function, *args))
self.timers[count] = timer
def timeout(self, _id, interval, function, *args):
function(*args)
if interval is None:
self.timers.pop(_id)
else:
self.timers[_id].setInterval(interval * 1000)
def want_stop(self):
self.app.quit()
def run(self):
self.app.exec_()
class QCallbacksManager(QObject):
class Request(object):
def __init__(self):
self.event = Event()
self.answer = None
def __call__(self):
raise NotImplementedError()
class LoginRequest(Request):
def __init__(self, backend_name, value):
QCallbacksManager.Request.__init__(self)
self.backend_name = backend_name
self.value = value
def __call__(self):
password, ok = QInputDialog.getText(None,
'%s request' % self.value.label,
'Please enter %s for %s' % (self.value.label,
self.backend_name),
QLineEdit.Password)
return password
def __init__(self, weboob, parent=None):
QObject.__init__(self, parent)
self.weboob = weboob
self.weboob.callbacks['login'] = self.callback(self.LoginRequest)
self.mutex = QMutex()
self.requests = []
self.connect(self, SIGNAL('new_request'), self.do_request)
def callback(self, klass):
def cb(*args, **kwargs):
return self.add_request(klass(*args, **kwargs))
return cb
def do_request(self):
self.mutex.lock()
request = self.requests.pop()
request.answer = request()
request.event.set()
self.mutex.unlock()
def add_request(self, request):
self.mutex.lock()
self.requests.append(request)
self.mutex.unlock()
self.emit(SIGNAL('new_request'))
request.event.wait()
return request.answer
class QtApplication(QApplication, Application):
def __init__(self):
QApplication.__init__(self, sys.argv)
self.setApplicationName(self.APPNAME)
Application.__init__(self)
self.cbmanager = QCallbacksManager(self.weboob, self)
def create_weboob(self):
return Weboob(scheduler=QtScheduler(self))
def load_backends(self, *args, **kwargs):
while True:
try:
return Application.load_backends(self, *args, **kwargs)
except VersionsMismatchError as e:
msg = 'Versions of modules mismatch with version of weboob.'
except ConfigError as e:
msg = unicode(e)
res = QMessageBox.question(None, 'Configuration error', u'%s\n\nDo you want to update repositories?' % msg, QMessageBox.Yes|QMessageBox.No)
if res == QMessageBox.No:
raise e
# Do not import it globally, it causes circular imports
from .backendcfg import ProgressDialog
pd = ProgressDialog('Update of repositories', "Cancel", 0, 100)
pd.setWindowModality(Qt.WindowModal)
try:
self.weboob.update(pd)
except ModuleInstallError as err:
QMessageBox.critical(None, self.tr('Update error'),
unicode(self.tr('Unable to update repositories: %s' % err)),
QMessageBox.Ok)
pd.setValue(100)
QMessageBox.information(None, self.tr('Update of repositories'),
self.tr('Repositories updated!'), QMessageBox.Ok)
class QtMainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
class QtDo(QObject):
def __init__(self, weboob, cb, eb=None):
QObject.__init__(self)
if not eb:
eb = self.default_eb
self.weboob = weboob
self.process = None
self.cb = cb
self.eb = eb
self.connect(self, SIGNAL('cb'), self.local_cb)
self.connect(self, SIGNAL('eb'), self.local_eb)
def do(self, *args, **kwargs):
self.process = self.weboob.do(*args, **kwargs)
self.process.callback_thread(self.thread_cb, self.thread_eb)
def default_eb(self, backend, error, backtrace):
if isinstance(error, MoreResultsAvailable):
# This is not an error, ignore.
return
msg = unicode(error)
if isinstance(error, BrowserIncorrectPassword):
if not msg:
msg = 'Invalid login/password.'
elif isinstance(error, BrowserUnavailable):
if not msg:
msg = 'Website is unavailable.'
elif isinstance(error, BrowserForbidden):
if not msg:
msg = 'This action is forbidden.'
elif isinstance(error, NotImplementedError):
msg = u'This feature is not supported by this backend.\n\n' \
u'To help the maintainer of this backend implement this feature, please contact: %s <%s>' % (backend.MAINTAINER, backend.EMAIL)
elif isinstance(error, UserError):
if not msg:
msg = type(error).__name__
elif logging.root.level == logging.DEBUG:
msg += u' '
ul_opened = False
for line in backtrace.split('\n'):
m = re.match(' File (.*)', line)
if m:
if not ul_opened:
msg += u'
'
print(error, file=sys.stderr)
print(backtrace, file=sys.stderr)
QMessageBox.critical(None, unicode(self.tr('Error with backend %s')) % backend.name,
msg, QMessageBox.Ok)
def local_cb(self, backend, data):
self.cb(backend, data)
if not backend:
self.disconnect(self, SIGNAL('cb'), self.local_cb)
self.disconnect(self, SIGNAL('eb'), self.local_eb)
self.process = None
def local_eb(self, backend, error, backtrace):
self.eb(backend, error, backtrace)
def thread_cb(self, backend, data):
self.emit(SIGNAL('cb'), backend, data)
def thread_eb(self, backend, error, backtrace):
self.emit(SIGNAL('eb'), backend, error, backtrace)
class HTMLDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
optionV4 = QStyleOptionViewItemV4(option)
self.initStyleOption(optionV4, index)
style = optionV4.widget.style() if optionV4.widget else QApplication.style()
doc = QTextDocument()
doc.setHtml(optionV4.text)
# painting item without text
optionV4.text = QString()
style.drawControl(QStyle.CE_ItemViewItem, optionV4, painter)
ctx = QAbstractTextDocumentLayout.PaintContext()
# Hilight text if item is selected
if optionV4.state & QStyle.State_Selected:
ctx.palette.setColor(QPalette.Text, optionV4.palette.color(QPalette.Active, QPalette.HighlightedText))
textRect = style.subElementRect(QStyle.SE_ItemViewItemText, optionV4)
painter.save()
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
optionV4 = QStyleOptionViewItemV4(option)
self.initStyleOption(optionV4, index)
doc = QTextDocument()
doc.setHtml(optionV4.text)
doc.setTextWidth(optionV4.rect.width())
return QSize(doc.idealWidth(), max(doc.size().height(), optionV4.decorationSize.height()))
class _QtValueStr(QLineEdit):
def __init__(self, value):
QLineEdit.__init__(self)
self._value = value
if value.default:
self.setText(unicode(value.default))
if value.masked:
self.setEchoMode(self.Password)
def set_value(self, value):
self._value = value
self.setText(self._value.get())
def get_value(self):
self._value.set(unicode(self.text()))
return self._value
class _QtValueBackendPassword(_QtValueStr):
def get_value(self):
self._value._domain = None
return _QtValueStr.get_value(self)
class _QtValueBool(QCheckBox):
def __init__(self, value):
QCheckBox.__init__(self)
self._value = value
if value.default:
self.setChecked(True)
def set_value(self, value):
self._value = value
self.setChecked(self._value.get())
def get_value(self):
self._value.set(self.isChecked())
return self._value
class _QtValueInt(QSpinBox):
def __init__(self, value):
QSpinBox.__init__(self)
self._value = value
if value.default:
self.setValue(int(value.default))
def set_value(self, value):
self._value = value
self.setValue(self._value.get())
def get_value(self):
self._value.set(self.getValue())
return self._value
class _QtValueChoices(QComboBox):
def __init__(self, value):
QComboBox.__init__(self)
self._value = value
for k, l in value.choices.iteritems():
self.addItem(l, QVariant(k))
if value.default == k:
self.setCurrentIndex(self.count()-1)
def set_value(self, value):
self._value = value
for i in xrange(self.count()):
if unicode(self.itemData(i).toString()) == self._value.get():
self.setCurrentIndex(i)
return
def get_value(self):
self._value.set(unicode(self.itemData(self.currentIndex()).toString()))
return self._value
def QtValue(value):
if isinstance(value, ValueBool):
klass = _QtValueBool
elif isinstance(value, ValueInt):
klass = _QtValueInt
elif isinstance(value, ValueBackendPassword):
klass = _QtValueBackendPassword
elif value.choices is not None:
klass = _QtValueChoices
else:
klass = _QtValueStr
return klass(copy(value))
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/qt/reposdlg.ui 0000664 0000000 0000000 00000002235 12411220504 0030110 0 ustar 00root root 0000000 0000000
RepositoriesDlg00400300RepositoriesQt::HorizontalQDialogButtonBox::Cancel|QDialogButtonBox::OkbuttonBoxrejected()RepositoriesDlgreject()316260286274
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/repl.py 0000664 0000000 0000000 00000134244 12411220504 0026630 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Christophe Benz, Romain Bignon, Laurent Bachelier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import atexit
from cmd import Cmd
import logging
import re
from optparse import OptionGroup, OptionParser, IndentedHelpFormatter
import os
from weboob.capabilities.base import FieldNotFound, BaseObject, UserError
from weboob.core import CallErrors
from weboob.tools.application.formatters.iformatter import MandatoryFieldsNotFound
from weboob.tools.misc import to_unicode
from weboob.tools.path import WorkingPath
from weboob.tools.ordereddict import OrderedDict
from weboob.capabilities.collection import Collection, BaseCollection, CapCollection, CollectionNotFound
from .console import BackendNotGiven, ConsoleApplication
from .formatters.load import FormattersLoader, FormatterLoadError
from .results import ResultsCondition, ResultsConditionError
__all__ = ['NotEnoughArguments', 'ReplApplication']
class NotEnoughArguments(Exception):
pass
class ReplOptionParser(OptionParser):
def format_option_help(self, formatter=None):
if not formatter:
formatter = self.formatter
return '%s\n%s' % (formatter.format_commands(self.commands),
OptionParser.format_option_help(self, formatter))
class ReplOptionFormatter(IndentedHelpFormatter):
def format_commands(self, commands):
s = u''
for section, cmds in commands.iteritems():
if len(cmds) == 0:
continue
if len(s) > 0:
s += '\n'
s += '%s Commands:\n' % section
for c in cmds:
c = c.split('\n')[0]
s += ' %s\n' % c
return s
def defaultcount(default_count=10):
def deco(f):
def inner(self, *args, **kwargs):
oldvalue = self.options.count
if self._is_default_count:
self.options.count = default_count
try:
return f(self, *args, **kwargs)
finally:
self.options.count = oldvalue
inner.__doc__ = f.__doc__
assert inner.__doc__ is not None, "A command must have a docstring"
inner.__doc__ += '\nDefault is limited to %s results.' % default_count
return inner
return deco
class ReplApplication(Cmd, ConsoleApplication):
"""
Base application class for Repl applications.
"""
SYNOPSIS = 'Usage: %prog [-dqv] [-b backends] [-cnfs] [command [arguments..]]\n'
SYNOPSIS += ' %prog [--help] [--version]'
DISABLE_REPL = False
EXTRA_FORMATTERS = {}
DEFAULT_FORMATTER = 'multiline'
COMMANDS_FORMATTERS = {}
# Objects to allow in do_ls / do_cd
COLLECTION_OBJECTS = tuple()
weboob_commands = set(['backends', 'condition', 'count', 'formatter', 'logging', 'select', 'quit', 'ls', 'cd'])
hidden_commands = set(['EOF'])
def __init__(self):
Cmd.__init__(self)
ConsoleApplication.__init__(self, ReplOptionParser(self.SYNOPSIS, version=self._get_optparse_version()))
self.intro = '\n'.join(('Welcome to %s%s%s v%s' % (self.BOLD, self.APPNAME, self.NC, self.VERSION),
'',
self.COPYRIGHT.encode(self.encoding),
'This program is free software: you can redistribute it and/or modify',
'it under the terms of the GNU Affero General Public License as published by',
'the Free Software Foundation, either version 3 of the License, or',
'(at your option) any later version.',
'',
'Type "help" to display available commands.',
'',
))
self.formatters_loader = FormattersLoader()
for key, klass in self.EXTRA_FORMATTERS.iteritems():
self.formatters_loader.register_formatter(key, klass)
self.formatter = None
self.commands_formatters = self.COMMANDS_FORMATTERS.copy()
commands_help = self.get_commands_doc()
self._parser.commands = commands_help
self._parser.formatter = ReplOptionFormatter()
results_options = OptionGroup(self._parser, 'Results Options')
results_options.add_option('-c', '--condition', help='filter result items to display given a boolean expression. See CONDITION section for the syntax')
results_options.add_option('-n', '--count', type='int',
help='limit number of results (from each backends)')
results_options.add_option('-s', '--select', help='select result item keys to display (comma separated)')
self._parser.add_option_group(results_options)
formatting_options = OptionGroup(self._parser, 'Formatting Options')
available_formatters = self.formatters_loader.get_available_formatters()
formatting_options.add_option('-f', '--formatter', choices=available_formatters,
help='select output formatter (%s)' % u', '.join(available_formatters))
formatting_options.add_option('--no-header', dest='no_header', action='store_true', help='do not display header')
formatting_options.add_option('--no-keys', dest='no_keys', action='store_true', help='do not display item keys')
formatting_options.add_option('-O', '--outfile', dest='outfile', help='file to export result')
self._parser.add_option_group(formatting_options)
self._interactive = False
self.working_path = WorkingPath()
self._change_prompt()
@property
def interactive(self):
return self._interactive
def _change_prompt(self):
self.objects = []
self.collections = []
# XXX can't use bold prompt because:
# 1. it causes problems when trying to get history (lines don't start
# at the right place).
# 2. when typing a line longer than term width, cursor goes at start
# of the same line instead of new line.
#self.prompt = self.BOLD + '%s> ' % self.APPNAME + self.NC
if len(self.working_path.get()):
wp_enc = unicode(self.working_path).encode(self.encoding)
self.prompt = '%s:%s> ' % (self.APPNAME, wp_enc)
else:
self.prompt = '%s> ' % (self.APPNAME)
def change_path(self, split_path):
self.working_path.location(split_path)
self._change_prompt()
def add_object(self, obj):
self.objects.append(obj)
def _complete_object(self):
return [obj.fullid for obj in self.objects]
def parse_id(self, id, unique_backend=False):
if self.interactive:
try:
obj = self.objects[int(id) - 1]
except (IndexError, ValueError):
# Try to find a shortcut in the cache
for obj in self.objects:
if id in obj.id:
id = obj.fullid
break
else:
if isinstance(obj, BaseObject):
id = obj.fullid
try:
return ConsoleApplication.parse_id(self, id, unique_backend)
except BackendNotGiven as e:
backend_name = None
while not backend_name:
print('This command works with an unique backend. Availables:')
for index, (name, backend) in enumerate(e.backends):
print('%s%d)%s %s%-15s%s %s' % (self.BOLD, index + 1, self.NC, self.BOLD, name, self.NC,
backend.DESCRIPTION))
i = self.ask('Select a backend to proceed with "%s"' % id)
if not i.isdigit():
if not i in dict(e.backends):
print('Error: %s is not a valid backend' % i, file=self.stderr)
continue
backend_name = i
else:
i = int(i)
if i < 0 or i > len(e.backends):
print('Error: %s is not a valid choice' % i, file=self.stderr)
continue
backend_name = e.backends[i-1][0]
return id, backend_name
def get_object(self, _id, method, fields=None, caps=None):
if self.interactive:
try:
obj = self.objects[int(_id) - 1]
except (IndexError, ValueError):
pass
else:
try:
backend = self.weboob.get_backend(obj.backend)
actual_method = getattr(backend, method, None)
if actual_method is None:
return None
else:
if callable(actual_method):
return backend.fillobj(obj, fields)
else:
return None
except UserError as e:
self.bcall_error_handler(backend, e, '')
_id, backend_name = self.parse_id(_id)
kargs = {}
if caps is not None:
kargs = {'caps': caps}
backend_names = (backend_name,) if backend_name is not None else self.enabled_backends
# if backend's service returns several objects, try to find the one
# with wanted ID. If not found, get the last not None object.
obj = None
# remove backends that do not have the required method
new_backend_names = []
for backend in backend_names:
if isinstance(backend, (str, unicode)):
actual_backend = self.weboob.get_backend(backend)
else:
actual_backend = backend
if getattr(actual_backend, method, None) is not None:
new_backend_names.append(backend)
backend_names = tuple(new_backend_names)
try:
for backend, objiter in self.do(method, _id, backends=backend_names, fields=fields, **kargs):
if objiter:
obj = objiter
if objiter.id == _id:
return obj
except CallErrors as e:
if obj is not None:
self.bcall_errors_handler(e)
else:
raise
return obj
def get_object_list(self, method=None, *args, **kwargs):
# return cache if not empty
if len(self.objects) > 0:
return self.objects
elif method is not None:
kwargs['backends'] = self.enabled_backends
for backend, object in self.weboob.do(self._do_complete, None, None, method, *args, **kwargs):
self.add_object(object)
return self.objects
# XXX: what can we do without method?
else:
return tuple()
def unload_backends(self, *args, **kwargs):
self.objects = []
self.collections = []
return ConsoleApplication.unload_backends(self, *args, **kwargs)
def load_backends(self, *args, **kwargs):
self.objects = []
self.collections = []
return ConsoleApplication.load_backends(self, *args, **kwargs)
def main(self, argv):
cmd_args = argv[1:]
if cmd_args:
cmd_line = u' '.join(cmd_args)
cmds = cmd_line.split(';')
for cmd in cmds:
ret = self.onecmd(cmd)
if ret:
return ret
elif self.DISABLE_REPL:
self._parser.print_help()
self._parser.exit()
else:
try:
import readline
except ImportError:
pass
else:
# Remove '-' from delims
readline.set_completer_delims(readline.get_completer_delims().replace('-', ''))
history_filepath = os.path.join(self.weboob.workdir, '%s_history' % self.APPNAME)
try:
readline.read_history_file(history_filepath)
except IOError:
pass
def savehist():
readline.write_history_file(history_filepath)
atexit.register(savehist)
self.intro += '\nLoaded backends: %s\n' % ', '.join(sorted(backend.name for backend in self.weboob.iter_backends()))
self._interactive = True
self.cmdloop()
def do(self, function, *args, **kwargs):
"""
Call Weboob.do(), passing count and selected fields given by user.
"""
backends = kwargs.pop('backends', None)
if backends is None:
kwargs['backends'] = []
for backend in self.enabled_backends:
actual_function = getattr(backend, function, None)
if actual_function is not None and callable(actual_function):
kwargs['backends'].append(backend)
else:
kwargs['backends'] = backends
fields = kwargs.pop('fields', self.selected_fields)
if not fields and fields != []:
fields = self.selected_fields
fields = self.parse_fields(fields)
if fields and self.formatter.MANDATORY_FIELDS is not None:
missing_fields = set(self.formatter.MANDATORY_FIELDS) - set(fields)
# If a mandatory field is not selected, do not use the customized formatter
if missing_fields:
print('Warning: you do not select enough mandatory fields for the formatter. Fallback to another. Hint: use option -f', file=self.stderr)
self.formatter = self.formatters_loader.build_formatter(ReplApplication.DEFAULT_FORMATTER)
if self.formatter.DISPLAYED_FIELDS is not None:
if fields is None:
missing_fields = True
else:
missing_fields = set(fields) - set(self.formatter.DISPLAYED_FIELDS + self.formatter.MANDATORY_FIELDS)
# If a selected field is not displayed, do not use the customized formatter
if missing_fields:
print('Warning: some selected fields will not be displayed by the formatter. Fallback to another. Hint: use option -f', file=self.stderr)
self.formatter = self.formatters_loader.build_formatter(ReplApplication.DEFAULT_FORMATTER)
return self.weboob.do(self._do_complete, self.options.count, fields, function, *args, **kwargs)
# -- command tools ------------
def parse_command_args(self, line, nb, req_n=None):
if line.strip() == '':
# because ''.split() = ['']
args = []
else:
args = line.strip().split(' ', nb - 1)
if req_n is not None and (len(args) < req_n):
raise NotEnoughArguments('Command needs %d arguments' % req_n)
if len(args) < nb:
args += tuple(None for i in xrange(nb - len(args)))
return args
# -- cmd.Cmd methods ---------
def postcmd(self, stop, line):
"""
This REPL method is overrided to return None instead of integers
to prevent stopping cmdloop().
"""
if not isinstance(stop, bool):
stop = None
return stop
def parseline(self, line):
"""
This REPL method is overrided to search "short" alias of commands
"""
cmd, arg, ignored = Cmd.parseline(self, line)
if cmd is not None:
names = set(name for name in self.get_names() if name.startswith('do_'))
if 'do_' + cmd not in names:
long = set(name for name in names if name.startswith('do_' + cmd))
# if more than one result, ambiguous command, do nothing (error will display suggestions)
if len(long) == 1:
cmd = long.pop()[3:]
return cmd, arg, ignored
def onecmd(self, line):
"""
This REPL method is overrided to catch some particular exceptions.
"""
line = to_unicode(line)
cmd, arg, ignored = self.parseline(line)
# Set the right formatter for the command.
try:
formatter_name = self.commands_formatters[cmd]
except KeyError:
formatter_name = self.DEFAULT_FORMATTER
self.set_formatter(formatter_name)
try:
try:
return super(ReplApplication, self).onecmd(line)
except CallErrors as e:
self.bcall_errors_handler(e)
except BackendNotGiven as e:
print('Error: %s' % str(e), file=self.stderr)
except NotEnoughArguments as e:
print('Error: not enough arguments. %s' % str(e), file=self.stderr)
except (KeyboardInterrupt, EOFError):
# ^C during a command process doesn't exit application.
print('\nAborted.')
finally:
self.flush()
def emptyline(self):
"""
By default, an emptyline repeats the previous command.
Overriding this function disables this behaviour.
"""
pass
def default(self, line):
print('Unknown command: "%s"' % line, file=self.stderr)
cmd, arg, ignore = Cmd.parseline(self, line)
if cmd is not None:
names = set(name[3:] for name in self.get_names() if name.startswith('do_' + cmd))
if len(names) > 0:
print('Do you mean: %s?' % ', '.join(names), file=self.stderr)
return 2
def completenames(self, text, *ignored):
return [name for name in Cmd.completenames(self, text, *ignored) if name not in self.hidden_commands]
def path_completer(self, arg):
dirname = os.path.dirname(arg)
try:
childs = os.listdir(dirname or '.')
except OSError:
return ()
l = []
for child in childs:
path = os.path.join(dirname, child)
if os.path.isdir(path):
child += '/'
l.append(child)
return l
def complete(self, text, state):
"""
Override of the Cmd.complete() method to:
* add a space at end of proposals
* display only proposals for words which match the
text already written by user.
"""
super(ReplApplication, self).complete(text, state)
# When state = 0, Cmd.complete() set the 'completion_matches' attribute by
# calling the completion function. Then, for other states, it only tries to
# get the right item in list.
# So that's the good place to rework the choices.
if state == 0:
self.completion_matches = [choice for choice in self.completion_matches if choice.startswith(text)]
try:
match = self.completion_matches[state]
except IndexError:
return None
else:
if match[-1] != '/':
return '%s ' % match
return match
# -- errors management -------------
def bcall_error_handler(self, backend, error, backtrace):
"""
Handler for an exception inside the CallErrors exception.
This method can be overrided to support more exceptions types.
"""
return super(ReplApplication, self).bcall_error_handler(backend, error, backtrace)
def bcall_errors_handler(self, errors, ignore=()):
if self.interactive:
ConsoleApplication.bcall_errors_handler(self, errors, 'Use "logging debug" option to print backtraces.', ignore)
else:
ConsoleApplication.bcall_errors_handler(self, errors, ignore=ignore)
# -- options related methods -------------
def _handle_options(self):
if self.options.formatter:
self.commands_formatters = {}
self.DEFAULT_FORMATTER = self.options.formatter
self.set_formatter(self.DEFAULT_FORMATTER)
if self.options.select:
self.selected_fields = self.options.select.split(',')
else:
self.selected_fields = ['$direct']
if self.options.count is not None:
self._is_default_count = False
if self.options.count <= 0:
# infinite search
self.options.count = None
if self.options.condition:
self.condition = ResultsCondition(self.options.condition)
else:
self.condition = None
return super(ReplApplication, self)._handle_options()
def get_command_help(self, command, short=False):
try:
func = getattr(self, 'do_' + command)
except AttributeError:
return None
doc = func.__doc__
assert doc is not None, "A command must have a docstring"
lines = [line.strip() for line in doc.strip().split('\n')]
if not lines[0].startswith(command):
lines = [command, ''] + lines
if short:
return lines[0]
return '\n'.join(lines)
def get_commands_doc(self):
names = set(name for name in self.get_names() if name.startswith('do_'))
appname = self.APPNAME.capitalize()
d = OrderedDict(((appname, []), ('Weboob', [])))
for name in sorted(names):
cmd = name[3:]
if cmd in self.hidden_commands.union(self.weboob_commands).union(['help']):
continue
d[appname].append(self.get_command_help(cmd))
if not self.DISABLE_REPL:
for cmd in self.weboob_commands:
d['Weboob'].append(self.get_command_help(cmd))
return d
# -- default REPL commands ---------
def do_quit(self, arg):
"""
Quit the application.
"""
return True
def do_EOF(self, arg):
"""
Quit the command line interpreter when ^D is pressed.
"""
# print empty line for the next shell prompt to appear on the first column of the terminal
print()
return self.do_quit(arg)
def do_help(self, arg=None):
"""
help [COMMAND]
List commands, or get information about a command.
"""
if arg:
cmd_names = set(name[3:] for name in self.get_names() if name.startswith('do_'))
if arg in cmd_names:
command_help = self.get_command_help(arg)
if command_help is None:
logging.warning(u'Command "%s" is undocumented' % arg)
else:
lines = command_help.split('\n')
lines[0] = '%s%s%s' % (self.BOLD, lines[0], self.NC)
self.stdout.write('%s\n' % '\n'.join(lines))
else:
print('Unknown command: "%s"' % arg, file=self.stderr)
else:
cmds = self._parser.formatter.format_commands(self._parser.commands)
self.stdout.write('%s\n' % cmds)
self.stdout.write('Type "help " for more info about a command.\n')
return 2
def complete_backends(self, text, line, begidx, endidx):
choices = []
commands = ['enable', 'disable', 'only', 'list', 'add', 'register', 'edit', 'remove', 'list-modules']
available_backends_names = set(backend.name for backend in self.weboob.iter_backends())
enabled_backends_names = set(backend.name for backend in self.enabled_backends)
args = line.split(' ')
if len(args) == 2:
choices = commands
elif len(args) >= 3:
if args[1] == 'enable':
choices = sorted(available_backends_names - enabled_backends_names)
elif args[1] == 'only':
choices = sorted(available_backends_names)
elif args[1] == 'disable':
choices = sorted(enabled_backends_names)
elif args[1] in ('add', 'register') and len(args) == 3:
for name, module in sorted(self.weboob.repositories.get_all_modules_info(self.CAPS).iteritems()):
choices.append(name)
elif args[1] == 'edit':
choices = sorted(available_backends_names)
elif args[1] == 'remove':
choices = sorted(available_backends_names)
return choices
def do_backends(self, line):
"""
backends [ACTION] [BACKEND_NAME]...
Select used backends.
ACTION is one of the following (default: list):
* enable enable given backends
* disable disable given backends
* only enable given backends and disable the others
* list list backends
* add add a backend
* register register a new account on a website
* edit edit a backend
* remove remove a backend
* list-modules list modules
"""
line = line.strip()
if line:
args = line.split()
else:
args = ['list']
action = args[0]
given_backend_names = args[1:]
for backend_name in given_backend_names:
if action in ('add', 'register'):
minfo = self.weboob.repositories.get_module_info(backend_name)
if minfo is None:
print('Module "%s" does not exist.' % backend_name, file=self.stderr)
return 1
else:
if not minfo.has_caps(self.CAPS):
print('Module "%s" is not supported by this application => skipping.' % backend_name, file=self.stderr)
return 1
else:
if backend_name not in [backend.name for backend in self.weboob.iter_backends()]:
print('Backend "%s" does not exist => skipping.' % backend_name, file=self.stderr)
return 1
if action in ('enable', 'disable', 'only', 'add', 'register', 'edit', 'remove'):
if not given_backend_names:
print('Please give at least a backend name.', file=self.stderr)
return 2
given_backends = set(backend for backend in self.weboob.iter_backends() if backend.name in given_backend_names)
if action == 'enable':
for backend in given_backends:
self.enabled_backends.add(backend)
elif action == 'disable':
for backend in given_backends:
try:
self.enabled_backends.remove(backend)
except KeyError:
print('%s is not enabled' % backend.name, file=self.stderr)
elif action == 'only':
self.enabled_backends = set()
for backend in given_backends:
self.enabled_backends.add(backend)
elif action == 'list':
enabled_backends_names = set(backend.name for backend in self.enabled_backends)
disabled_backends_names = set(backend.name for backend in self.weboob.iter_backends()) - enabled_backends_names
print('Enabled: %s' % ', '.join(enabled_backends_names))
if len(disabled_backends_names) > 0:
print('Disabled: %s' % ', '.join(disabled_backends_names))
elif action == 'add':
for name in given_backend_names:
instname = self.add_backend(name)
if instname:
self.load_backends(names=[instname])
elif action == 'register':
for name in given_backend_names:
instname = self.register_backend(name)
if isinstance(instname, basestring):
self.load_backends(names=[instname])
elif action == 'edit':
for backend in given_backends:
enabled = backend in self.enabled_backends
self.unload_backends(names=[backend.name])
self.edit_backend(backend.name)
for newb in self.load_backends(names=[backend.name]).itervalues():
if not enabled:
self.enabled_backends.remove(newb)
elif action == 'remove':
for backend in given_backends:
self.weboob.backends_config.remove_backend(backend.name)
self.unload_backends(backend.name)
elif action == 'list-modules':
modules = []
print('Modules list:')
for name, info in sorted(self.weboob.repositories.get_all_modules_info().iteritems()):
if not self.is_module_loadable(info):
continue
modules.append(name)
loaded = ' '
for bi in self.weboob.iter_backends():
if bi.NAME == name:
if loaded == ' ':
loaded = 'X'
elif loaded == 'X':
loaded = 2
else:
loaded += 1
print('[%s] %s%-15s%s %s' % (loaded, self.BOLD, name, self.NC, info.description))
else:
print('Unknown action: "%s"' % action, file=self.stderr)
return 1
if len(self.enabled_backends) == 0:
print('Warning: no more backends are loaded. %s is probably unusable.' % self.APPNAME.capitalize(), file=self.stderr)
def complete_logging(self, text, line, begidx, endidx):
levels = ('debug', 'info', 'warning', 'error', 'quiet', 'default')
args = line.split(' ')
if len(args) == 2:
return levels
return ()
def do_logging(self, line):
"""
logging [LEVEL]
Set logging level.
Availables: debug, info, warning, error.
* quiet is an alias for error
* default is an alias for warning
"""
args = self.parse_command_args(line, 1, 0)
levels = (('debug', logging.DEBUG),
('info', logging.INFO),
('warning', logging.WARNING),
('error', logging.ERROR),
('quiet', logging.ERROR),
('default', logging.WARNING)
)
if not args[0]:
current = None
for label, level in levels:
if logging.root.level == level:
current = label
break
print('Current level: %s' % current)
return
levels = dict(levels)
try:
level = levels[args[0]]
except KeyError:
print('Level "%s" does not exist.' % args[0], file=self.stderr)
print('Availables: %s' % ' '.join(levels.iterkeys()), file=self.stderr)
return 2
else:
logging.root.setLevel(level)
for handler in logging.root.handlers:
handler.setLevel(level)
def do_condition(self, line):
"""
condition [EXPRESSION | off]
If an argument is given, set the condition expression used to filter the results. See CONDITION section for more details and the expression.
If the "off" value is given, conditional filtering is disabled.
If no argument is given, print the current condition expression.
"""
line = line.strip()
if line:
if line == 'off':
self.condition = None
else:
try:
self.condition = ResultsCondition(line)
except ResultsConditionError as e:
print('%s' % e, file=self.stderr)
return 2
else:
if self.condition is None:
print('No condition is set.')
else:
print(str(self.condition))
def do_count(self, line):
"""
count [NUMBER | off]
If an argument is given, set the maximum number of results fetched.
NUMBER must be at least 1.
"off" value disables counting, and allows infinite searches.
If no argument is given, print the current count value.
"""
line = line.strip()
if line:
if line == 'off':
self.options.count = None
self._is_default_count = False
else:
try:
count = int(line)
except ValueError:
print('Could not interpret "%s" as a number.' % line, file=self.stderr)
return 2
else:
if count > 0:
self.options.count = count
self._is_default_count = False
else:
self.options.count = None
self._is_default_count = False
else:
if self.options.count is None:
print('Counting disabled.')
else:
print(self.options.count)
def complete_formatter(self, text, line, *ignored):
formatters = self.formatters_loader.get_available_formatters()
commands = ['list', 'option'] + formatters
options = ['header', 'keys']
option_values = ['on', 'off']
args = line.split(' ')
if len(args) == 2:
return commands
if args[1] == 'option':
if len(args) == 3:
return options
if len(args) == 4:
return option_values
elif args[1] in formatters:
return list(set(name[3:] for name in self.get_names() if name.startswith('do_')))
def do_formatter(self, line):
"""
formatter [list | FORMATTER [COMMAND] | option OPTION_NAME [on | off]]
If a FORMATTER is given, set the formatter to use.
You can add a COMMAND to apply the formatter change only to
a given command.
If the argument is "list", print the available formatters.
If the argument is "option", set the formatter options.
Valid options are: header, keys.
If on/off value is given, set the value of the option.
If not, print the current value for the option.
If no argument is given, print the current formatter.
"""
args = line.strip().split()
if args:
if args[0] == 'list':
print(', '.join(self.formatters_loader.get_available_formatters()))
elif args[0] == 'option':
if len(args) > 1:
if len(args) == 2:
if args[1] == 'header':
print('off' if self.options.no_header else 'on')
elif args[1] == 'keys':
print('off' if self.options.no_keys else 'on')
else:
if args[2] not in ('on', 'off'):
print('Invalid value "%s". Please use "on" or "off" values.' % args[2], file=self.stderr)
return 2
else:
if args[1] == 'header':
self.options.no_header = True if args[2] == 'off' else False
elif args[1] == 'keys':
self.options.no_keys = True if args[2] == 'off' else False
else:
print('Don\'t know which option to set. Available options: header, keys.', file=self.stderr)
return 2
else:
if args[0] in self.formatters_loader.get_available_formatters():
if len(args) > 1:
self.commands_formatters[args[1]] = self.set_formatter(args[0])
else:
self.commands_formatters = {}
self.DEFAULT_FORMATTER = self.set_formatter(args[0])
else:
print('Formatter "%s" is not available.\n' \
'Available formatters: %s.' % (args[0], ', '.join(self.formatters_loader.get_available_formatters())), file=self.stderr)
return 1
else:
print('Default formatter: %s' % self.DEFAULT_FORMATTER)
for key, klass in self.commands_formatters.iteritems():
print('Command "%s": %s' % (key, klass))
def do_select(self, line):
"""
select [FIELD_NAME]... | "$direct" | "$full"
If an argument is given, set the selected fields.
$direct selects all fields loaded in one http request.
$full selects all fields using as much http requests as necessary.
If no argument is given, print the currently selected fields.
"""
line = line.strip()
if line:
split = line.split()
self.selected_fields = split
else:
print(' '.join(self.selected_fields))
# First sort in alphabetical of backend
# Second, sort with ID
def comp_object(self, obj1, obj2):
if obj1.backend == obj2.backend:
if obj1.id == obj2.id:
return 0
elif obj1.id > obj2.id:
return 1
else:
return -1
elif obj1.backend > obj2.backend:
return 1
else:
return -1
@defaultcount(40)
def do_ls(self, line):
"""
ls [-d] [-U] [PATH]
List objects in current path.
If an argument is given, list the specified path.
Use -U option to not sort results. It allows you to use a "fast path" to
return results as soon as possible.
Use -d option to display information about a collection (and to not
display the content of it). It has the same behavior than the well
know UNIX "ls" command.
"""
# TODO: real parsing of options
path = line.strip()
only = False
sort = True
if '-U' in line.strip().partition(' '):
path = line.strip().partition(' ')[-1]
sort = False
if '-d' in line.strip().partition(' '):
path = None
only = line.strip().partition(' ')[-1]
if path:
for _path in path.split('/'):
# We have an argument, let's ch to the directory before the ls
self.working_path.cd1(_path)
objects = []
collections = []
self.objects = []
self.start_format()
for res in self._fetch_objects(objs=self.COLLECTION_OBJECTS):
if isinstance(res, Collection):
collections.append(res)
if sort is False:
self.formatter.format_collection(res, only)
else:
if sort:
objects.append(res)
else:
self._format_obj(res, only)
if sort:
objects.sort(cmp=self.comp_object)
collections = self._merge_collections_with_same_path(collections)
collections.sort(cmp=self.comp_object)
for collection in collections:
self.formatter.format_collection(collection, only)
for obj in objects:
self._format_obj(obj, only)
if path:
for _path in path.split('/'):
# Let's go back to the parent directory
self.working_path.up()
else:
# Save collections only if we listed the current path.
self.collections = collections
def _find_collection(self, collection, collections):
for col in collections:
if col.split_path == collection.split_path:
return col
return None
def _merge_collections_with_same_path(self, collections):
to_return = []
for collection in collections:
col = self._find_collection(collection, to_return)
if col:
col.backend += " %s" % collection.backend
else:
to_return.append(collection)
return to_return
def _format_obj(self, obj, only):
if only is False or not hasattr(obj, 'id') or obj.id in only:
self.cached_format(obj)
def do_cd(self, line):
"""
cd [PATH]
Follow a path.
".." is a special case and goes up one directory.
"" is a special case and goes home.
"""
if not len(line.strip()):
self.working_path.home()
elif line.strip() == '..':
self.working_path.up()
else:
self.working_path.cd1(line)
collections = []
try:
for backend, res in self.do('get_collection', objs=self.COLLECTION_OBJECTS,
split_path=self.working_path.get(),
caps=CapCollection):
if res:
collections.append(res)
except CallErrors as errors:
self.bcall_errors_handler(errors, CollectionNotFound)
if len(collections):
# update the path from the collection if possible
if len(collections) == 1:
self.working_path.split_path = collections[0].split_path
else:
print(u"Path: %s not found" % unicode(self.working_path), file=self.stderr)
self.working_path.restore()
return 1
self._change_prompt()
def _fetch_objects(self, objs):
split_path = self.working_path.get()
try:
for backend, res in self.do('iter_resources', objs=objs,
split_path=split_path,
caps=CapCollection):
yield res
except CallErrors as errors:
self.bcall_errors_handler(errors, CollectionNotFound)
def all_collections(self):
"""
Get all objects that are collections: regular objects and fake dumb objects.
"""
obj_collections = [obj for obj in self.objects if isinstance(obj, BaseCollection)]
return obj_collections + self.collections
def obj_to_filename(self, obj, dest=None, default=None):
"""
This method can be used to get a filename from an object, using a mask
filled by information of this object.
All patterns are braces-enclosed, and are name of available fields in
the object.
:param obj: object
:type obj: BaseObject
:param dest: dest given by user (default None)
:type dest: str
:param default: default file mask (if not given, this is '{id}-{title}.{ext}')
:type default: str
:rtype: str
"""
if default is None:
default = '{id}-{title}.{ext}'
if dest is None:
dest = '.'
if os.path.isdir(dest):
dest = os.path.join(dest, default)
def repl(m):
field = m.group(1)
if hasattr(obj, field):
return re.sub('[?:/]', '-', '%s' % getattr(obj, field))
else:
return m.group(0)
return re.sub(r'\{(.+?)\}', repl, dest)
# for cd & ls
def complete_path(self, text, line, begidx, endidx):
directories = set()
if len(self.working_path.get()):
directories.add('..')
mline = line.partition(' ')[2]
offs = len(mline) - len(text)
# refresh only if needed
if len(self.objects) == 0 and len(self.collections) == 0:
try:
self.objects, self.collections = self._fetch_objects(objs=self.COLLECTION_OBJECTS)
except CallErrors as errors:
self.bcall_errors_handler(errors, CollectionNotFound)
collections = self.all_collections()
for collection in collections:
directories.add(collection.basename.encode(self.encoding))
return [s[offs:] for s in directories if s.startswith(mline)]
def complete_ls(self, text, line, begidx, endidx):
return self.complete_path(text, line, begidx, endidx)
def complete_cd(self, text, line, begidx, endidx):
return self.complete_path(text, line, begidx, endidx)
# -- formatting related methods -------------
def set_formatter(self, name):
"""
Set the current formatter from name.
It returns the name of the formatter which has been really set.
"""
try:
self.formatter = self.formatters_loader.build_formatter(name)
except FormatterLoadError as e:
print('%s' % e, file=self.stderr)
if self.DEFAULT_FORMATTER == name:
self.DEFAULT_FORMATTER = ReplApplication.DEFAULT_FORMATTER
print('Falling back to "%s".' % (self.DEFAULT_FORMATTER), file=self.stderr)
self.formatter = self.formatters_loader.build_formatter(self.DEFAULT_FORMATTER)
name = self.DEFAULT_FORMATTER
if self.options.no_header:
self.formatter.display_header = False
if self.options.no_keys:
self.formatter.display_keys = False
if self.options.outfile:
self.formatter.outfile = self.options.outfile
if self.interactive:
self.formatter.interactive = True
return name
def set_formatter_header(self, string):
pass
def start_format(self, **kwargs):
self.formatter.start_format(**kwargs)
def cached_format(self, obj):
self.add_object(obj)
alias = None
if self.interactive:
alias = '%s' % len(self.objects)
self.format(obj, alias=alias)
def parse_fields(self, fields):
if '$direct' in fields:
return []
if '$full' in fields:
return None
return fields
def format(self, result, alias=None):
fields = self.parse_fields(self.selected_fields)
try:
self.formatter.format(obj=result, selected_fields=fields, alias=alias)
except FieldNotFound as e:
print(e, file=self.stderr)
except MandatoryFieldsNotFound as e:
print('%s Hint: select missing fields or use another formatter (ex: multiline).' % e, file=self.stderr)
def flush(self):
self.formatter.flush()
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/application/results.py 0000664 0000000 0000000 00000011103 12411220504 0027353 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.capabilities import UserError
__all__ = ['ResultsCondition', 'ResultsConditionError']
class IResultsCondition(object):
def is_valid(self, obj):
raise NotImplementedError()
class ResultsConditionError(UserError):
pass
class Condition(object):
def __init__(self, left, op, right):
self.left = left # Field of the object to test
self.op = op
self.right = right
def is_egal(left, right):
return left == right
def is_notegal(left, right):
return left != right
def is_sup(left, right):
return left < right
def is_inf(left, right):
return left > right
def is_in(left, right):
return left in right
functions = {'!=': is_notegal, '=': is_egal, '>': is_sup, '<': is_inf, '|': is_in}
class ResultsCondition(IResultsCondition):
condition_str = None
# Supported operators
# =, !=, <, > for float/int/decimal
# =, != for strings
# We build a list of list. Return true if each conditions of one list is TRUE
def __init__(self, condition_str):
or_list = []
for _or in condition_str.split(' OR '):
and_list = []
for _and in _or.split(' AND '):
operator = None
for op in ['!=', '=', '>', '<', '|']:
if op in _and:
operator = op
break
if operator is None:
raise ResultsConditionError(u'Could not find a valid operator in sub-expression "%s". Protect the complete condition expression with quotes, or read the documentation in the man manual.' % _and)
try:
l, r = _and.split(operator)
except ValueError:
raise ResultsConditionError(u'Syntax error in the condition expression, please check documentation')
and_list.append(Condition(l, operator, r))
or_list.append(and_list)
self.condition = or_list
self.condition_str = condition_str
def is_valid(self, obj):
import weboob.tools.date as date_utils
from datetime import date
d = obj.to_dict()
# We evaluate all member of a list at each iteration.
for _or in self.condition:
myeval = True
for condition in _or:
if condition.left in d:
# in the case of id, test id@backend and id
if condition.left == 'id':
tocompare = condition.right
evalfullid = functions[condition.op](tocompare, d['id'])
evalid = functions[condition.op](tocompare, obj.id)
myeval = evalfullid or evalid
else:
# We have to change the type of v, always gived as string by application
typed = type(d[condition.left])
try:
if isinstance(d[condition.left], date_utils.date):
tocompare = date(*[int(x) for x in condition.right.split('-')])
else:
tocompare = typed(condition.right)
myeval = functions[condition.op](tocompare, d[condition.left])
except:
myeval = False
else:
raise ResultsConditionError(u'Field "%s" is not valid.' % condition.left)
# Do not try all AND conditions if one is false
if not myeval:
break
# Return True at the first OR valid condition
if myeval:
return True
# If we are here, all OR conditions are False
return False
def __str__(self):
return unicode(self).encode('utf-8')
def __unicode__(self):
return self.condition_str
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/backend.py 0000664 0000000 0000000 00000032603 12411220504 0024746 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
import os
from threading import RLock
from copy import copy
from weboob.capabilities.base import BaseObject, FieldNotFound, \
CapBase, NotLoaded, NotAvailable
from weboob.tools.misc import iter_fields
from weboob.tools.log import getLogger
from weboob.tools.value import ValuesDict
__all__ = ['BackendStorage', 'BackendConfig', 'Module']
class BackendStorage(object):
"""
This is an abstract layer to store data in storages (:mod:`weboob.tools.storage`)
easily.
It is instancied automatically in constructor of :class:`Module`, in the
:attr:`Module.storage` attribute.
:param name: name of backend
:param storage: storage object
:type storage: :class:`weboob.tools.storage.IStorage`
"""
def __init__(self, name, storage):
self.name = name
self.storage = storage
def set(self, *args):
"""
Set value in the storage.
Example:
>>> from weboob.tools.storage import StandardStorage
>>> backend = BackendStorage('blah', StandardStorage('/tmp/cfg'))
>>> backend.storage.set('config', 'nb_of_threads', 10)
>>>
:param args: the path where to store value
"""
if self.storage:
return self.storage.set('backends', self.name, *args)
def delete(self, *args):
"""
Delete a value from the storage.
:param args: path to delete.
"""
if self.storage:
return self.storage.delete('backends', self.name, *args)
def get(self, *args, **kwargs):
"""
Get a value or a dict of values in storage.
Example:
>>> from weboob.tools.storage import StandardStorage
>>> backend = BackendStorage('blah', StandardStorage('/tmp/cfg'))
>>> backend.storage.get('config', 'nb_of_threads')
10
>>> backend.storage.get('config', 'unexistant', 'path', default='lol')
'lol'
>>> backend.storage.get('config')
{'nb_of_threads': 10, 'other_things': 'blah'}
:param args: path to get
:param default: if specified, default value when path is not found
"""
if self.storage:
return self.storage.get('backends', self.name, *args, **kwargs)
else:
return kwargs.get('default', None)
def load(self, default):
"""
Load storage.
It is made automatically when your backend is created, and use the
``STORAGE`` class attribute as default.
:param default: this is the default tree if storage is empty
:type default: :class:`dict`
"""
if self.storage:
return self.storage.load('backends', self.name, default)
def save(self):
"""
Save storage.
"""
if self.storage:
return self.storage.save('backends', self.name)
class BackendConfig(ValuesDict):
"""
Configuration of a backend.
This class is firstly instanced as a :class:`weboob.tools.value.ValuesDict`,
containing some :class:`weboob.tools.value.Value` (and derivated) objects.
Then, using the :func:`load` method will load configuration from file and
create a copy of the :class:`BackendConfig` object with the loaded values.
"""
modname = None
instname = None
weboob = None
def load(self, weboob, modname, instname, config, nofail=False):
"""
Load configuration from dict to create an instance.
:param weboob: weboob object
:type weboob: :class:`weboob.core.ouiboube.Weboob`
:param modname: name of the module
:type modname: :class:`str`
:param instname: name of this backend
:type instname: :class:`str`
:param params: parameters to load
:type params: :class:`dict`
:param nofail: if true, this call can't fail
:type nofail: :class:`bool`
:rtype: :class:`BackendConfig`
"""
cfg = BackendConfig()
cfg.modname = modname
cfg.instname = instname
cfg.weboob = weboob
for name, field in self.iteritems():
value = config.get(name, None)
if value is None:
if not nofail and field.required:
raise Module.ConfigError('Backend(%s): Configuration error: Missing parameter "%s" (%s)'
% (cfg.instname, name, field.description))
value = field.default
field = copy(field)
try:
field.load(cfg.instname, value, cfg.weboob.callbacks)
except ValueError as v:
if not nofail:
raise Module.ConfigError(
'Backend(%s): Configuration error for field "%s": %s' % (cfg.instname, name, v))
cfg[name] = field
return cfg
def dump(self):
"""
Dump config in a dictionary.
:rtype: :class:`dict`
"""
settings = {}
for name, value in self.iteritems():
settings[name] = value.dump()
return settings
def save(self, edit=True, params=None):
"""
Save backend config.
:param edit: if true, it changes config of an existing backend
:type edit: :class:`bool`
:param params: if specified, params to merge with the ones of the current object
:type params: :class:`dict`
"""
assert self.modname is not None
assert self.instname is not None
assert self.weboob is not None
dump = self.dump()
if params is not None:
dump.update(params)
self.weboob.backends_config.add_backend(self.instname, self.modname, dump, edit)
class Module(object):
"""
Base class for backends.
You may derivate it, and also all capabilities you want to implement.
:param weboob: weboob instance
:type weboob: :class:`weboob.core.ouiboube.Weboob`
:param name: name of backend
:type name: :class:`str`
:param config: configuration of backend
:type config: :class:`dict`
:param storage: storage object
:type storage: :class:`weboob.tools.storage.IStorage`
:param logger: logger
:type logger: :class:`logging.Logger`
"""
# Module name.
NAME = None
# Name of the maintainer of this module.
MAINTAINER = u''
# Email address of the maintainer.
EMAIL = ''
# Version of module (for information only).
VERSION = ''
# Description
DESCRIPTION = ''
# License of this module.
LICENSE = ''
# Configuration required for backends.
# Values must be weboob.tools.value.Value objects.
CONFIG = BackendConfig()
# Storage
STORAGE = {}
# Browser class
BROWSER = None
# URL to an optional icon.
# If you want to create your own icon, create a 'favicon.ico' ico in
# the module's directory, and keep the ICON value to None.
ICON = None
# Supported objects to fill
# The key is the class and the value the method to call to fill
# Method prototype: method(object, fields)
# When the method is called, fields are only the one which are
# NOT yet filled.
OBJECTS = {}
class ConfigError(Exception):
"""
Raised when the config can't be loaded.
"""
def __enter__(self):
self.lock.acquire()
def __exit__(self, t, v, tb):
self.lock.release()
def __repr__(self):
return u"" % self.name
def __init__(self, weboob, name, config=None, storage=None, logger=None):
self.logger = getLogger(name, parent=logger)
self.weboob = weboob
self.name = name
self.lock = RLock()
# Private fields (which start with '_')
self._private_config = dict((key, value) for key, value in config.iteritems() if key.startswith('_'))
# Load configuration of backend.
self.config = self.CONFIG.load(weboob, self.NAME, self.name, config)
self.storage = BackendStorage(self.name, storage)
self.storage.load(self.STORAGE)
def deinit(self):
"""
This abstract method is called when the backend is unloaded.
"""
pass
_browser = None
@property
def browser(self):
"""
Attribute 'browser'. The browser is created at the first call
of this attribute, to avoid useless pages access.
Note that the :func:`create_default_browser` method is called to create it.
"""
if self._browser is None:
self._browser = self.create_default_browser()
return self._browser
def create_default_browser(self):
"""
Method to overload to build the default browser in
attribute 'browser'.
"""
return self.create_browser()
def create_browser(self, *args, **kwargs):
"""
Build a browser from the BROWSER class attribute and the
given arguments.
"""
if not self.BROWSER:
return None
tmpproxy = None
tmpproxys = None
if '_proxy' in self._private_config:
tmpproxy = self._private_config['_proxy']
elif 'http_proxy' in os.environ:
tmpproxy = os.environ['http_proxy']
elif 'HTTP_PROXY' in os.environ:
tmpproxy = os.environ['HTTP_PROXY']
if '_proxy_ssl' in self._private_config:
tmpproxys = self._private_config['_proxy_ssl']
elif 'https_proxy' in os.environ:
tmpproxys = os.environ['https_proxy']
elif 'HTTPS_PROXY' in os.environ:
tmpproxys = os.environ['HTTPS_PROXY']
if any((tmpproxy, tmpproxys)):
kwargs['proxy'] = {}
if tmpproxy is not None:
kwargs['proxy']['http'] = tmpproxy
if tmpproxys is not None:
kwargs['proxy']['https'] = tmpproxys
kwargs['logger'] = self.logger
if self.logger.settings['responses_dirname']:
kwargs.setdefault('responses_dirname', os.path.join(self.logger.settings['responses_dirname'],
self._private_config.get('_debug_dir', self.name)))
return self.BROWSER(*args, **kwargs)
@classmethod
def iter_caps(klass):
"""
Iter capabilities implemented by this backend.
:rtype: iter[:class:`weboob.capabilities.base.CapBase`]
"""
def iter_caps(cls):
for base in cls.__bases__:
if issubclass(base, CapBase) and base != CapBase:
yield base
for cap in iter_caps(base):
yield cap
return iter_caps(klass)
def has_caps(self, *caps):
"""
Check if this backend implements at least one of these capabilities.
"""
for c in caps:
if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \
isinstance(self, c):
return True
return False
def fillobj(self, obj, fields=None):
"""
Fill an object with the wanted fields.
:param fields: what fields to fill; if None, all fields are filled
:type fields: :class:`list`
"""
if obj is None:
return obj
def not_loaded(v):
return (v is NotLoaded or isinstance(v, BaseObject) and not v.__iscomplete__())
if isinstance(fields, basestring):
fields = (fields,)
missing_fields = []
if fields is None:
# Select all fields
if isinstance(obj, BaseObject):
fields = [item[0] for item in obj.iter_fields()]
else:
fields = [item[0] for item in iter_fields(obj)]
for field in fields:
if not hasattr(obj, field):
raise FieldNotFound(obj, field)
value = getattr(obj, field)
missing = False
if hasattr(value, '__iter__'):
for v in (value.itervalues() if isinstance(value, dict) else value):
if not_loaded(v):
missing = True
break
elif not_loaded(value):
missing = True
if missing:
missing_fields.append(field)
if not missing_fields:
return obj
for key, value in self.OBJECTS.iteritems():
if isinstance(obj, key):
self.logger.debug(u'Fill %r with fields: %s' % (obj, missing_fields))
return value(self, obj, missing_fields) or obj
# Object is not supported by backend. Do not notice it to avoid flooding user.
# That's not so bad.
for field in missing_fields:
setattr(obj, field, NotAvailable)
return obj
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/browser/ 0000775 0000000 0000000 00000000000 12411220504 0024464 5 ustar 00root root 0000000 0000000 woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/browser/__init__.py 0000664 0000000 0000000 00000002706 12411220504 0026602 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Christophe Benz
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from weboob.tools.browser.browser import BrowserIncorrectPassword, BrowserBanned, \
BrowserUnavailable, BrowserRetry, \
BrowserHTTPNotFound, BrowserHTTPError, \
BasePage, BaseBrowser, BrokenPageError, \
StandardBrowser, BrowserPasswordExpired, \
BrowserForbidden
__all__ = ['BrowserIncorrectPassword', 'BrowserPasswordExpired', 'BrowserBanned',
'BrowserUnavailable', 'BrowserRetry', 'BrowserHTTPNotFound', 'BrowserHTTPError',
'BasePage', 'BaseBrowser', 'BrokenPageError', 'StandardBrowser', 'BrowserForbidden']
woob-a64c9f2edbf92a3324949216696259e9415031e4-weboob/weboob/tools/browser/browser.py 0000664 0000000 0000000 00000062300 12411220504 0026522 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2010-2014 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see .
from __future__ import print_function
import sys
if sys.version_info >= (3,0):
raise ImportError("This module isn't compatible with python3")
from copy import copy
from httplib import BadStatusLine
try:
import mechanize
except ImportError:
raise ImportError('Please install python-mechanize')
import os
import re
from threading import RLock
import ssl
import httplib
import socket
import hashlib
import time
import urllib
import urllib2
import mimetypes
import logging
from contextlib import closing
from gzip import GzipFile
import warnings
from weboob.tools.exceptions import BrowserUnavailable, BrowserIncorrectPassword, BrowserPasswordExpired, BrowserForbidden, BrowserBanned, BrowserHTTPNotFound, BrowserHTTPError, FormFieldConversionWarning, BrowserSSLError
from weboob.tools.decorators import retry
from weboob.tools.log import getLogger
from weboob.tools.mech import ClientForm
ControlNotFoundError = ClientForm.ControlNotFoundError
from weboob.tools.parsers import get_parser
__all__ = ['BrowserIncorrectPassword', 'BrowserForbidden', 'BrowserBanned', 'BrowserUnavailable', 'BrowserRetry',
'BrowserPasswordExpired', 'BrowserHTTPNotFound', 'BrowserHTTPError', 'BrokenPageError', 'BasePage',
'StandardBrowser', 'BaseBrowser']
class BrowserRetry(Exception):
pass
class NoHistory(object):
"""
We don't want to fill memory with history
"""
def __init__(self):
pass
def add(self, request, response):
pass
def back(self, n, _response):
pass
def clear(self):
pass
def close(self):
pass
class BrokenPageError(Exception):
pass
class BasePage(object):
"""
Base page
"""
ENCODING = None
def __init__(self, browser, document, url='', groups=None, group_dict=None, logger=None):
self.browser = browser
self.parser = browser.parser
self.document = document
self.url = url
self.groups = groups
self.group_dict = group_dict
self.logger = getLogger('page', logger)
def on_loaded(self):
"""
Called when the page is loaded.
"""
pass
def check_location(func):
def inner(self, *args, **kwargs):
if args and isinstance(args[0], basestring):
url = args[0]
if url.startswith('/') and hasattr(self, 'DOMAIN') and (not self.request or self.request.host != self.DOMAIN):
url = '%s://%s%s' % (self.PROTOCOL, self.DOMAIN, url)
url = re.sub('(.*)#.*', r'\1', url)
if isinstance(url, unicode):
url = url.encode('utf-8')
args = (url,) + args[1:]
return func(self, *args, **kwargs)
return inner
class StandardBrowser(mechanize.Browser):
"""
Standard Browser.
:param firefox_cookies: path to cookies sqlite file
:type firefox_cookies: str
:param parser: parser to use on HTML files
:type parser: :class:`weboob.tools.parsers.iparser.IParser`
:param history: history manager; default value is an object which
does not keep history
:type history: object
:param proxy: proxy URL to use
:type proxy: str
:param factory: mechanize factory. None to use Mechanize's default
:type factory: object
"""
# ------ Class attributes --------------------------------------
ENCODING = 'utf-8'
USER_AGENTS = {
'desktop_firefox': 'Mozilla/5.0 (X11; Linux x86_64; rv:31.0) Gecko/20100101 Firefox/31.0',
'android': 'Mozilla/5.0 (Linux; U; Android 2.1; en-us; Nexus One Build/ERD62) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17',
'microb': 'Mozilla/5.0 (X11; U; Linux armv7l; fr-FR; rv:1.9.2.3pre) Gecko/20100723 Firefox/3.5 Maemo Browser 1.7.4.8 RX-51 N900',
'wget': 'Wget/1.11.4',
}
USER_AGENT = USER_AGENTS['desktop_firefox']
DEBUG_HTTP = False
DEBUG_MECHANIZE = False
DEFAULT_TIMEOUT = 15
INSECURE = False # if True, do not validate SSL
logger = None
# ------ Browser methods ---------------------------------------
# I'm not a robot, so disable the check of permissions in robots.txt.
default_features = copy(mechanize.Browser.default_features)
default_features.remove('_robots')
default_features.remove('_refresh')
def __init__(self, firefox_cookies=None, parser=None, history=NoHistory(), proxy=None, logger=None, factory=None, responses_dirname=None):
mechanize.Browser.__init__(self, history=history, factory=factory)
self.logger = getLogger('browser', logger)
self.addheaders = [
['User-agent', self.USER_AGENT]
]
# Use a proxy
self.proxy = proxy
if proxy is not None:
self.set_proxies(proxy)
# Share cookies with firefox
if firefox_cookies:
# Try to load cookies
try:
from .firefox_cookies import FirefoxCookieJar
self._cookie = FirefoxCookieJar(self.DOMAIN, firefox_cookies)
self._cookie.load()
self.set_cookiejar(self._cookie)
except ImportError as e:
logging.warning("Unable to store Firefox cookies: %s", e)
self._cookie = None
else:
self._cookie = None
if parser is None:
parser = get_parser()()
elif isinstance(parser, (tuple,list,basestring)):
parser = get_parser(parser)()
self.parser = parser
self.lock = RLock()
if self.DEBUG_HTTP:
# display messages from httplib
self.set_debug_http(True)
if logging.root.level == logging.DEBUG:
# Enable log messages from mechanize.Browser
self.set_debug_redirects(True)
mech_logger = logging.getLogger("mechanize")
mech_logger.setLevel(logging.INFO)
self.responses_dirname = responses_dirname
self.responses_count = 0
def __enter__(self):
self.lock.acquire()
def __exit__(self, t, v, tb):
self.lock.release()
def _openurl(self, *args, **kwargs):
return mechanize.Browser.open(self, *args, **kwargs)
@check_location
@retry(BrowserHTTPError, tries=3)
def openurl(self, *args, **kwargs):
"""
Open an URL but do not create a Page object.
"""
if_fail = kwargs.pop('if_fail', 'raise')
self.logger.debug('Opening URL "%s", %s' % (args, kwargs))
kwargs['timeout'] = kwargs.get('timeout', self.DEFAULT_TIMEOUT)
try:
return self._openurl(*args, **kwargs)
except (mechanize.BrowserStateError, mechanize.response_seek_wrapper,
urllib2.HTTPError, urllib2.URLError, BadStatusLine, ssl.SSLError) as e:
if isinstance(e, mechanize.BrowserStateError) and hasattr(self, 'home'):
self.home()
return self._openurl(*args, **kwargs)
elif if_fail == 'raise':
raise self.get_exception(e)('%s (url="%s")' % (e, args and args[0] or 'None'))
else:
return None
except BrowserRetry as e:
return self._openurl(*args, **kwargs)
def get_exception(self, e):
if isinstance(e, urllib2.HTTPError) and hasattr(e, 'getcode'):
if e.getcode() in (404, 403):
return BrowserHTTPNotFound
if e.getcode() == 401:
return BrowserIncorrectPassword
elif isinstance(e, mechanize.BrowserStateError):
return BrowserHTTPNotFound
return BrowserHTTPError
def readurl(self, url, *args, **kwargs):
"""
Download URL data specifying what to do on failure (nothing by default).
"""
if not 'if_fail' in kwargs:
kwargs['if_fail'] = None
result = self.openurl(url, *args, **kwargs)
if result:
if self.logger.settings['save_responses']:
self.save_response(result)
return result.read()
else:
return None
def save_response(self, result, warning=False):
"""
Save a stream to a temporary file, and log its name.
The stream is rewinded after saving.
"""
if self.responses_dirname is None:
import tempfile
self.responses_dirname = tempfile.mkdtemp(prefix='weboob_session_')
print('Debug data will be saved in this directory: %s' % self.responses_dirname, file=sys.stderr)
elif not os.path.isdir(self.responses_dirname):
os.makedirs(self.responses_dirname)
# get the content-type, remove optionnal charset part
mimetype = result.info().get('Content-Type', '').split(';')[0]
# due to http://bugs.python.org/issue1043134
if mimetype == 'text/plain':
ext = '.txt'
else:
# try to get an extension (and avoid adding 'None')
ext = mimetypes.guess_extension(mimetype, False) or ''
response_filepath = os.path.join(self.responses_dirname, unicode(self.responses_count)+ext)
with open(response_filepath, 'w') as f:
f.write(result.read())
result.seek(0)
match_filepath = os.path.join(self.responses_dirname, 'url_response_match.txt')
with open(match_filepath, 'a') as f:
f.write('%s\t%s\n' % (result.geturl(), os.path.basename(response_filepath)))
self.responses_count += 1
msg = u'Response saved to %s' % response_filepath
if warning:
self.logger.warning(msg)
else:
self.logger.info(msg)
def get_document(self, result, parser=None, encoding=None):
"""
Get a parsed document from a stream.
:param result: HTML page stream
:type result: stream
"""
if parser is None:
parser = self.parser
elif isinstance(parser, (basestring, list, tuple)):
parser = get_parser(parser)()
if encoding is None:
encoding = self.ENCODING
return parser.parse(result, encoding)
def location(self, *args, **kwargs):
"""
Go on an URL and get the related document.
"""
return self.get_document(self.openurl(*args, **kwargs))
@staticmethod
def buildurl(base, *args, **kwargs):
"""
Build an URL and escape arguments.
You can give a serie of tuples in args (and the order is keept), or
a dict in kwargs (but the order is lost).
Example:
>>> StandardBrowser.buildurl('/blah.php', ('a', '&'), ('b', '='))
'/blah.php?a=%26&b=%3D'
>>> StandardBrowser.buildurl('/blah.php', a='&', b='=')
'/blah.php?b=%3D&a=%26'
"""
if not args:
args = kwargs
if not args:
return base
else:
return '%s?%s' % (base, urllib.urlencode(args))
def str(self, s):
if isinstance(s, unicode):
s = s.encode('iso-8859-15', 'replace')
return s
def set_field(self, args, label, field=None, value=None, is_list=False):
"""
Set a value to a form field.
:param args: arguments where to look for value
:type args: dict
:param label: label in args
:type label: str
:param field: field name. If None, use label instead
:type field: str
:param value: value to give on field
:type value: str
:param is_list: the field is a list
:type is_list: bool
"""
try:
if not field:
field = label
if args.get(label, None) is not None:
if not value:
if is_list:
if isinstance(is_list, (list, tuple)):
try:
value = [self.str(is_list.index(args[label]))]
except ValueError as e:
if args[label]:
print('[%s] %s: %s' % (label, args[label], e), file=sys.stderr)
return
else:
value = [self.str(args[label])]
else:
value = self.str(args[label])
self[field] = value
except ControlNotFoundError:
return
def lowsslcheck(self, domain, hsh):
if self.INSECURE or (self.logger is not None and self.logger.settings['ssl_insecure']):
return
certhash = self._certhash(domain)
if self.logger:
self.logger.debug('Found %s as certificate hash' % certhash)
if isinstance(hsh, basestring):
hsh = [hsh]
if certhash not in hsh:
raise BrowserSSLError()
def _certhash(self, domain, port=443):
certs = ssl.get_server_certificate((domain, port))
return hashlib.sha256(certs).hexdigest()
def __setitem__(self, key, value):
if isinstance(value, unicode):
value = value.encode(self.ENCODING or 'utf-8')
warnings.warn('Implicit conversion of form field %r from unicode to str' % key,
FormFieldConversionWarning, stacklevel=2)
if self.form is None:
raise AttributeError('Please select a form before setting values to fields')
return self.form.__setitem__(key, value)
class BaseBrowser(StandardBrowser):
"""
Base browser class to navigate on a website.
:param username: username on website
:type username: str
:param password: password on website. If it is None, Browser will
not try to login
:type password: str
:param firefox_cookies: path to cookies sqlite file
:type firefox_cookies: str
:param parser: parser to use on HTML files
:type parser: :class:`weboob.tools.parsers.iparser.IParser`
:param history: history manager; default value is an object which
does not keep history
:type history: object
:param proxy: proxy URL to use
:type proxy: dictionnary
:param logger: logger to use for logging
:type logger: :class:`logging.Logger`
:param factory: mechanize factory. None to use Mechanize's default
:type factory: object
:param get_home: try to get the homepage.
:type get_homme: bool
:param responses_dirname: directory to store responses
:type responses_dirname: str
"""
# ------ Class attributes --------------------------------------
DOMAIN = None
PROTOCOL = 'http'
PAGES = {}
# SHA-256 hash of server certificate. If set, it will automatically check it,
# and raise a SSLError exception if it doesn't match.
CERTHASH = None
# ------ Abstract methods --------------------------------------
def home(self):
"""
Go to the home page.
"""
if self.DOMAIN is not None:
self.location('%s://%s/' % (self.PROTOCOL, self.DOMAIN))
def login(self):
"""
Login to the website.
This function is called when is_logged() returns False and the password
attribute is not None.
"""
raise NotImplementedError()
def is_logged(self):
"""
Return True if we are logged on website. When Browser tries to access
to a page, if this method returns False, it calls login().
It is never called if the password attribute is None.
"""
raise NotImplementedError()
# ------ Browser methods ---------------------------------------
def __init__(self, username=None, password=None, firefox_cookies=None,
parser=None, history=NoHistory(), proxy=None, logger=None,
factory=None, get_home=True, responses_dirname=None):
StandardBrowser.__init__(self, firefox_cookies, parser, history, proxy, logger, factory, responses_dirname)
self.page = None
self.last_update = 0.0
self.username = username
self.password = password
if self.CERTHASH is not None and self.DOMAIN is not None:
self.lowsslcheck(self.DOMAIN, self.CERTHASH)
if self.password and get_home:
try:
self.home()
# Do not abort the build of browser when the website is down.
except BrowserUnavailable:
pass
def submit(self, *args, **kwargs):
"""
Submit the selected form.
"""
no_login = kwargs.pop('nologin', kwargs.pop('no_login', False))
try:
self._change_location(mechanize.Browser.submit(self, *args, **kwargs), no_login=no_login)
except (mechanize.response_seek_wrapper, urllib2.HTTPError, urllib2.URLError, BadStatusLine, ssl.SSLError) as e:
self.page = None
raise self.get_exception(e)(e)
except (mechanize.BrowserStateError, BrowserRetry) as e:
raise BrowserUnavailable(e)
def is_on_page(self, pageCls):
"""
Check the current page.
:param pageCls: class of the page to check
:type pageCls: :class:`BasePage`
:rtype: bool
"""
return isinstance(self.page, pageCls)
def absurl(self, rel):
"""
Get an absolute URL from a relative one.
"""
if rel is None:
return None
if not rel.startswith('/'):
rel = '/' + rel
return '%s://%s%s' % (self.PROTOCOL, self.DOMAIN, rel)
def follow_link(self, *args, **kwargs):
"""
Follow a link on the page.
"""
try:
self._change_location(mechanize.Browser.follow_link(self, *args, **kwargs))
except (mechanize.response_seek_wrapper, urllib2.HTTPError, urllib2.URLError, BadStatusLine, ssl.SSLError) as e:
self.page = None
raise self.get_exception(e)('%s (url="%s")' % (e, args and args[0] or 'None'))
except (mechanize.BrowserStateError, BrowserRetry) as e:
self.home()
raise BrowserUnavailable(e)
def _openurl(self, *args, **kwargs):
return mechanize.Browser.open_novisit(self, *args, **kwargs)
@check_location
@retry(BrowserHTTPError, tries=3)
def location(self, *args, **kwargs):
"""
Change location of browser on an URL.
When the page is loaded, it looks up PAGES to find a regexp which
matches, and create the object. Then, the 'on_loaded' method of
this object is called.
If a password is set, and is_logged() returns False, it tries to login
with login() and reload the page.
"""
keep_args = copy(args)
keep_kwargs = kwargs.copy()
no_login = kwargs.pop('no_login', kwargs.pop('nologin', False))
kwargs['timeout'] = kwargs.get('timeout', self.DEFAULT_TIMEOUT)
try:
self._change_location(mechanize.Browser.open(self, *args, **kwargs), no_login=no_login)
except BrowserRetry:
if not self.page or not args or self.page.url != args[0]:
keep_kwargs['no_login'] = True
self.location(*keep_args, **keep_kwargs)
except (mechanize.response_seek_wrapper, urllib2.HTTPError, urllib2.URLError, BadStatusLine, ssl.SSLError) as e:
self.page = None
raise self.get_exception(e)('%s (url="%s")' % (e, args and args[0] or 'None'))
except mechanize.BrowserStateError:
self.home()
self.location(*keep_args, **keep_kwargs)
# DO NOT ENABLE THIS FUCKING PEACE OF CODE EVEN IF IT WOULD BE BETTER
# TO SANITARIZE FUCKING HTML.
#def _set_response(self, response, *args, **kwargs):
# import time
# if response and hasattr(response, 'set_data'):
# print time.time()
# r = response.read()
# start = 0
# end = 0
# new = ''
# lowr = r.lower()
# start = lowr[end:].find('')
# new += r[start_stop:end].replace('<', '<').replace('>', '>')
# start = end + lowr[end:].find('