pax_global_header 0000666 0000000 0000000 00000000064 13726205233 0014515 g ustar 00root root 0000000 0000000 52 comment=8259210387867d750dd4cc420c7b063d313198f2
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/ 0000775 0000000 0000000 00000000000 13726205233 0020303 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/ 0000775 0000000 0000000 00000000000 13726205233 0021560 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/__init__.py 0000664 0000000 0000000 00000000351 13726205233 0023670 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
__title__ = 'weboob'
__version__ = '2.1'
__author__ = 'The Weboob Association'
__copyright__ = 'Copyright 2012-2019 The Weboob Association'
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/ 0000775 0000000 0000000 00000000000 13726205233 0024246 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/__init__.py 0000664 0000000 0000000 00000000102 13726205233 0026350 0 ustar 00root root 0000000 0000000 import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobands/ 0000775 0000000 0000000 00000000000 13726205233 0026035 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobands/__init__.py 0000664 0000000 0000000 00000001431 13726205233 0030145 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2018 Quentin Defenouillere
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobands import Boobands
__all__ = ['Boobands']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobands/boobands.py 0000664 0000000 0000000 00000014676 13726205233 0030214 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2018 Quentin Defenouillere
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from weboob.capabilities.base import empty
from weboob.capabilities.bands import CapBands
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Boobands', 'BandInfoFormatter', 'BandListFormatter', 'FavoritesFormatter']
class BandInfoFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'genre', 'year', 'country', 'description')
def format_obj(self, obj, alias):
result = u'\n%s%s%s\n' % (self.BOLD, obj.name, self.NC)
if not empty(obj.genre):
result += 'Genre: %s\n' % obj.genre
if not empty(obj.country):
result += 'Country: %s\n' % obj.country
if not empty(obj.year):
result += 'Formed in: %s\n' % obj.year
if not empty(obj.description):
result += '%sDescription:%s\n' % (self.BOLD, self.NC)
result += '%s\n' % obj.description
return result.strip()
class BandListFormatter(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 += '%s\n' % obj.short_description
result+='---------------------------------------------------------------------------'
return result.strip()
class FavoritesFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'band_url', 'short_description')
def get_title(self, obj):
return obj.name
def get_description(self, obj):
result = u''
if not empty(obj.short_description):
result += '%s\n' % obj.short_description
if not empty(obj.band_url):
result += '\t%s\n' % obj.band_url
result+='---------------------------------------------------------------------------'
return result.strip()
class AlbumsFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'album_type', 'year', 'reviews')
def format_obj(self, obj, alias):
result = u'\n%s%s%s\n' % (self.BOLD, obj.name, self.NC)
if not empty(obj.album_type):
result += 'Album type: %s\n' % obj.album_type
if not empty(obj.id):
result += 'Link to album: %s\n' % obj.id
if not empty(obj.year):
result += 'Year of release: %s\n' % obj.year
if not empty(obj.reviews):
result += 'Reviews: %s\n' % obj.reviews
result+='---------------------------------------------------------------------------'
return result.strip()
class SuggestionsFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'name', 'description', 'url')
def get_title(self, obj):
return obj.name
def get_description(self, obj):
result = u''
if not empty(obj.description):
result += '%s\n' % obj.description
if not empty(obj.url):
result += '\tLink to band: %s\n' % obj.url
result+='---------------------------------------------------------------------------'
return result.strip()
class Boobands(ReplApplication):
APPNAME = 'boobands'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2018-YEAR Quentin Defenouillere'
DESCRIPTION = "Console application allowing to display music bands and offer music suggestions."
SHORT_DESCRIPTION = "Display bands and suggestions"
CAPS = CapBands
DEFAULT_FORMATTER = 'table'
EXTRA_FORMATTERS = {
'band_search': BandListFormatter,
'band_info': BandInfoFormatter,
'get_favorites': FavoritesFormatter,
'get_albums': AlbumsFormatter,
'suggestion': SuggestionsFormatter
}
COMMANDS_FORMATTERS = {
'search': 'band_search',
'info': 'band_info',
'favorites': 'get_favorites',
'albums': 'get_albums',
'suggestions': 'suggestion'
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
@defaultcount(20)
def do_search(self, pattern):
"""
band PATTERN
Search bands.
"""
self.change_path(['search'])
self.start_format()
for band in self.do('iter_band_search', pattern, caps=CapBands):
self.cached_format(band)
def do_info(self, line):
"""
info BAND_ID
Get detailed info for specified band. Use the 'search' command to find bands.
"""
band, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(band)
self.start_format()
for info in self.do('get_info', _id, backends=backend_name, caps=CapBands):
if info:
self.format(info)
@defaultcount(40)
def do_albums(self, line):
"""
albums BAND_ID
Get the discography of a band.
"""
albums, = self.parse_command_args(line, 1, 1)
_id, backend_name = self.parse_id(albums)
self.start_format()
for album in self.do('get_albums', _id, backends=backend_name, caps=CapBands):
self.cached_format(album)
@defaultcount(100)
def do_favorites(self, *ignored):
"""
favorites
Displays your favorite bands.
"""
self.change_path(['favorites'])
self.start_format()
for favorite in self.do('get_favorites', caps=CapBands):
self.cached_format(favorite)
@defaultcount(100)
def do_suggestions(self, *ignored):
"""
suggestions
Suggests bands depending on your favorite bands.
"""
self.change_path(['suggestions'])
self.start_format()
for suggestion in self.do('suggestions', caps=CapBands):
self.cached_format(suggestion)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobank/ 0000775 0000000 0000000 00000000000 13726205233 0025661 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobank/__init__.py 0000664 0000000 0000000 00000001426 13726205233 0027775 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobank import Boobank
__all__ = ['Boobank']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobank/boobank.py 0000664 0000000 0000000 00000115340 13726205233 0027652 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from contextlib import contextmanager
import datetime
import uuid
from dateutil.relativedelta import relativedelta
from dateutil.parser import parse as parse_date
from decimal import Decimal, InvalidOperation
from weboob.browser.browsers import APIBrowser
from weboob.browser.profiles import Weboob
from weboob.exceptions import (
BrowserHTTPError, CaptchaQuestion, DecoupledValidation,
AppValidationCancelled, AppValidationExpired,
)
from weboob.core.bcall import CallErrors
from weboob.capabilities.base import empty, find_object
from weboob.capabilities.bank import (
Account, Transaction,
Transfer, TransferStep, Recipient, AddRecipientStep,
CapBank, CapTransfer,
TransferInvalidLabel, TransferInvalidAmount, TransferInvalidDate,
TransferInvalidEmitter, TransferInvalidRecipient,
)
from weboob.capabilities.wealth import CapBankWealth
from weboob.capabilities.captcha import exception_to_job
from weboob.capabilities.profile import CapProfile
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.captcha import CaptchaMixin
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.tools.compat import getproxies
from weboob.tools.log import getLogger
from weboob.tools.misc import to_unicode
__all__ = ['Boobank']
class OfxFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'rdate', 'label', 'raw', 'amount', 'category')
TYPES_ACCTS = {
Account.TYPE_CHECKING: 'CHECKING',
Account.TYPE_SAVINGS: 'SAVINGS',
Account.TYPE_DEPOSIT: 'DEPOSIT',
Account.TYPE_LOAN: 'LOAN',
Account.TYPE_MARKET: 'MARKET',
Account.TYPE_JOINT: 'JOINT',
Account.TYPE_CARD: 'CARD',
}
TYPES_TRANS = {
Transaction.TYPE_TRANSFER: 'DIRECTDEP',
Transaction.TYPE_ORDER: 'PAYMENT',
Transaction.TYPE_CHECK: 'CHECK',
Transaction.TYPE_DEPOSIT: 'DEP',
Transaction.TYPE_PAYBACK: 'OTHER',
Transaction.TYPE_WITHDRAWAL: 'ATM',
Transaction.TYPE_CARD: 'POS',
Transaction.TYPE_LOAN_PAYMENT: 'INT',
Transaction.TYPE_BANK: 'FEE',
}
balance = Decimal(0)
coming = Decimal(0)
account_type = ''
seen = set()
def start_format(self, **kwargs):
account = kwargs['account']
self.balance = account.balance
self.coming = account.coming
self.account_type = account.type
self.output(u'OFXHEADER:100')
self.output(u'DATA:OFXSGML')
self.output(u'VERSION:102')
self.output(u'SECURITY:NONE')
self.output(u'ENCODING:UTF-8')
self.output(u'CHARSET:UTF-8')
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'))
if self.account_type == Account.TYPE_CARD:
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)
self.output(u'%s' % self.account_type)
self.output(u'null')
self.output('')
else:
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)
self.output(u'%s' % self.account_type)
self.output(u'null')
self.output('')
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):
# special case of coming operations with card ID
result = u'\n'
if hasattr(obj, '_coming') and obj._coming and hasattr(obj, 'obj._cardid') and not empty(obj._cardid):
result += u'%s\n' % obj._cardid
elif obj.type in self.TYPES_TRANS:
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')
if obj.rdate:
result += u'%s\n' % obj.rdate.strftime('%Y%m%d')
result += u'%s\n' % obj.amount
result += u'%s\n' % obj.unique_id(self.seen)
if hasattr(obj, 'label') and not empty(obj.label):
result += u'%s\n' % obj.label.replace('&', '&')
else:
result += u'%s\n' % obj.raw.replace('&', '&')
if obj.category:
result += u'%s\n' % obj.category.replace('&', '&')
result += u''
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'))
self.output(u'')
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'))
if self.account_type == Account.TYPE_CARD:
self.output(u'')
else:
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 or ''
if not label and hasattr(obj, 'raw'):
label = obj.raw or ''
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', 'exec_date', 'account_label', 'recipient_label', 'amount')
DISPLAYED_FIELDS = ('label', 'account_iban', 'recipient_iban', 'currency')
def format_obj(self, obj, alias):
result = u'------- Transfer %s -------\n' % obj.fullid
result += u'Date: %s\n' % obj.exec_date
if obj.account_iban:
result += u'Origin: %s (%s)\n' % (obj.account_label, obj.account_iban)
else:
result += u'Origin: %s\n' % obj.account_label
if obj.recipient_iban:
result += u'Recipient: %s (%s)\n' % (obj.recipient_label, obj.recipient_iban)
else:
result += u'Recipient: %s\n' % obj.recipient_label
result += u'Amount: %.2f %s\n' % (obj.amount, obj.currency or '')
if obj.label:
result += u'Label: %s\n' % obj.label
return result
class TransferListFormatter(IFormatter):
def format_obj(self, obj, alias):
result = [
u'From: %s' % self.colored('%-20s' % obj.account_label, 'red'),
u' Label: %s\n' % self.colored(obj.label, 'yellow'),
u'To: %s' % self.colored('%-22s' % obj.recipient_label, 'green'),
u' Amount: %s\n' % self.colored(obj.amount, 'red'),
u'Date: %s' % self.colored(obj.exec_date, 'yellow'),
u' Status: %s' % self.colored(obj.status, 'yellow'),
'\n',
]
return ''.join(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 check_emptyness(self, obj):
if not empty(obj):
return (obj, '%11.2f')
return ('---', '%11s')
def format_obj(self, obj, alias):
label = obj.label
if not empty(obj.diff):
diff = obj.diff
elif not empty(obj.quantity) and not empty(obj.unitprice):
diff = obj.valuation - (obj.quantity * obj.unitprice)
else:
diff = '---'
format_diff = '%8s'
if isinstance(diff, Decimal):
format_diff = '%8.2f'
self.tot_diff += diff
if not empty(obj.quantity):
quantity = obj.quantity
format_quantity = '%11.2f'
if obj.quantity == obj.quantity.to_integral():
format_quantity = '%11d'
else:
format_quantity = '%11s'
quantity = '---'
unitvalue, format_unitvalue = self.check_emptyness(obj.unitvalue)
valuation, format_valuation = self.check_emptyness(obj.valuation)
if isinstance(valuation, Decimal):
self.tot_valuation += obj.valuation
if empty(obj.code) and not empty(obj.description):
code = obj.description
else:
code = obj.code
return u' %s %s %s %s %s %s' % \
(self.colored('%-30s' % label[:30], 'red'),
self.colored('%-12s' % code[:12], 'yellow') if not empty(code) else ' ' * 12,
self.colored(format_quantity % quantity, 'yellow'),
self.colored(format_unitvalue % unitvalue, 'yellow'),
self.colored(format_valuation % valuation, 'yellow'),
self.colored(format_diff % diff, 'green' if not isinstance(diff, str) and diff >= 0 else 'red')
)
def flush(self):
self.output(u'-------------------------------+--------------+------------+------------+------------+---------')
self.output(u' Total %s %s' %
(self.colored('%11.2f' % self.tot_valuation, 'yellow'),
self.colored('%9.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')
DISPLAYED_FIELDS = ('iban', 'bank_name')
def start_format(self, **kwargs):
self.output('Available recipients:')
def get_title(self, obj):
details = ' - '.join(filter(None, (obj.iban, obj.bank_name)))
if details:
return '%s (%s)' % (obj.label, details)
return obj.label
class AdvisorListFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'name')
def start_format(self, **kwargs):
self.output(' Bank Name Contacts')
self.output('-----------------+------------------------------+-----------------------------------------')
def format_obj(self, obj, alias):
bank = obj.backend
phones = ""
contacts = []
if not empty(obj.phone):
phones += obj.phone
if not empty(obj.mobile):
if phones != "":
phones += " or %s" % obj.mobile
else:
phones += obj.mobile
if phones:
contacts.append(phones)
for attr in ('email', 'agency', 'address'):
value = getattr(obj, attr)
if not empty(value):
contacts.append(value)
if len(contacts) > 0:
first_contact = contacts.pop(0)
else:
first_contact = ""
result = u" %s %s %s " % (self.colored('%-15s' % bank, 'yellow'),
self.colored('%-30s' % obj.name, 'red'),
self.colored("%-30s" % first_contact, 'green'))
for contact in contacts:
result += "\n %s %s" % ((" ") * 47, self.colored("%-25s" % contact, 'green'))
return result
class AccountListFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'label', 'balance', 'coming', 'type')
totals = {}
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')
currency = obj.currency or 'EUR'
result = u'%s %s %s %s' % (id,
self.colored('%-25s' % obj.label[:25], 'yellow' if obj.type != Account.TYPE_LOAN else 'blue'),
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 '')
currency_totals = self.totals.setdefault(currency, {})
currency_totals.setdefault('balance', Decimal(0))
currency_totals.setdefault('coming', Decimal(0))
if obj.type != Account.TYPE_LOAN:
currency_totals['balance'] += balance
currency_totals['coming'] += coming
return result
def flush(self):
self.output(u'------------------------------------------%s+----------+----------' % (('-' * 15) if not self.interactive else ''))
for currency, currency_totals in sorted(self.totals.items(), key=lambda k_v: (k_v[1]['balance'], k_v[1]['coming'], k_v[0])):
balance = currency_totals['balance']
coming = currency_totals['coming']
self.output(u'%s Total (%s) %s %s' % (
(' ' * 15) if not self.interactive else '',
currency,
self.colored('%8.2f' % balance, 'green' if balance >= 0 else 'red'),
self.colored('%8.2f' % coming, 'green' if coming >= 0 else 'red'))
)
self.totals.clear()
class EmitterListFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'label', 'currency')
def start_format(self, **kwargs):
self.output(
' %s Emitter Currency Number Type Number Balance ' % (
(' ' * 15) if not self.interactive else ''
)
)
self.output(
'----------------------------%s+----------+-------------+-------------+----------+' % (
('-' * 15) if not self.interactive else ''
)
)
def format_emitter_number(self, obj):
account_number = ' '*11
if obj.number_type != 'unknown' and obj.number:
account_number = '%s***%s' % (obj.number[:4], obj.number[len(obj.number) - 4:])
return account_number
def format_obj(self, obj, alias):
if alias is not None:
obj_id = '%s' % self.colored('%3s' % ('#' + alias), 'red', 'bold')
else:
obj_id = self.colored('%30s' % obj.fullid, 'red', 'bold')
balance = ' ' * 9
if not empty(obj.balance):
balance = self.colored('%9.2f' % obj.balance, 'green' if obj.balance >= 0 else 'red')
account_number = self.format_emitter_number(obj)
return u'%s %s %s %s %s %s' % (
obj_id,
self.colored('%-25s' % obj.label[:25], 'yellow', 'bold'),
self.colored('%-10s' % obj.currency, 'green', 'bold'),
self.colored('%-13s' % obj.number_type, 'blue', 'bold'),
self.colored('%-11s' % account_number, 'blue', 'bold'),
balance
)
def flush(self):
self.output(
u'----------------------------%s+----------+-------------+-------------+----------+' % (
('-' * 15) if not self.interactive else ''
)
)
class Boobank(CaptchaMixin, ReplApplication):
APPNAME = 'boobank'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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,
'advisor_list': AdvisorListFormatter,
'transfer_list': TransferListFormatter,
'emitter_list': EmitterListFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'ls': 'account_list',
'list': 'account_list',
'recipients': 'recipient_list',
'transfer': 'transfer',
'history': 'ops_list',
'coming': 'ops_list',
'transfer_history': 'transfer_list',
'investment': 'investment_list',
'advisor': 'advisor_list',
'emitters': 'emitter_list',
}
COLLECTION_OBJECTS = (Account, Transaction, )
def bcall_error_handler(self, backend, error, backtrace):
if isinstance(error, TransferStep):
params = {}
for field in error.fields:
v = self.ask(field)
params[field.id] = v
#backend.config['accept_transfer'].set(v)
params['backends'] = backend
self.start_format()
for transfer in self.do('transfer', error.transfer, **params):
self.format(transfer)
elif isinstance(error, AddRecipientStep):
params = {}
params['backends'] = backend
for field in error.fields:
v = self.ask(field)
params[field.id] = v
try:
next(iter(self.do('add_recipient', error.recipient, **params)))
except CallErrors as e:
self.bcall_errors_handler(e)
elif isinstance(error, DecoupledValidation):
if isinstance(error.resource, Recipient):
func_name = 'add_recipient'
elif isinstance(error.resource, Transfer):
func_name = 'transfer'
else:
print(u'Error(%s): The resource should be of type Recipient or Transfer, not "%s"' % (backend.name, type(error.resource)), file=self.stderr)
return False
print(error.message)
params = {
'backends': backend,
'resume': True,
}
try:
next(iter(self.do(func_name, error.resource, **params)))
except CallErrors as e:
self.bcall_errors_handler(e)
elif isinstance(error, AppValidationCancelled):
print(u'Error(%s): %s' % (backend.name, to_unicode(error) or 'The app validation has been cancelled'), file=self.stderr)
elif isinstance(error, AppValidationExpired):
print(u'Error(%s): %s' % (backend.name, to_unicode(error) or 'The app validation has expired'), file=self.stderr)
elif isinstance(error, TransferInvalidAmount):
print(u'Error(%s): %s' % (backend.name, to_unicode(error) or 'The transfer amount is invalid'), file=self.stderr)
elif isinstance(error, TransferInvalidLabel):
print(u'Error(%s): %s' % (backend.name, to_unicode(error) or 'The transfer label is invalid'), file=self.stderr)
elif isinstance(error, TransferInvalidEmitter):
print(u'Error(%s): %s' % (backend.name, to_unicode(error) or 'The transfer emitter is invalid'), file=self.stderr)
elif isinstance(error, TransferInvalidRecipient):
print(u'Error(%s): %s' % (backend.name, to_unicode(error) or 'The transfer recipient is invalid'), file=self.stderr)
elif isinstance(error, TransferInvalidDate):
print(u'Error(%s): %s' % (backend.name, to_unicode(error) or 'The transfer execution date is invalid'), file=self.stderr)
elif isinstance(error, CaptchaQuestion):
if not self.captcha_weboob.count_backends():
print('Error(%s): Site requires solving a CAPTCHA but no CapCaptchaSolver backends were configured' % backend.name, file=self.stderr)
return False
print('Info(%s): Encountered CAPTCHA, please wait for its resolution, it can take dozens of seconds' % backend.name, file=self.stderr)
job = exception_to_job(error)
self.solve_captcha(job, backend)
return False
else:
return super(Boobank, self).bcall_error_handler(backend, error, backtrace)
def load_default_backends(self):
self.load_backends(CapBank, storage=self.create_storage())
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 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_add_recipient(self, line):
"""
add_recipient iban label
Add a recipient to a backend.
"""
if len(self.enabled_backends) > 1:
print('Error: select a single backend to add a recipient (Hint: try the command "backends only")', file=self.stderr)
return 1
iban, label, origin_account_id = self.parse_command_args(line, 3, 2)
recipient = Recipient()
recipient.iban = iban
recipient.label = label
recipient.origin_account_id = origin_account_id
next(iter(self.do('add_recipient', recipient)))
def do_recipients(self, line):
"""
recipients ACCOUNT
List recipients of ACCOUNT
"""
id_from, = self.parse_command_args(line, 1, 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
self.objects = []
self.start_format()
for recipient in self.do('iter_transfer_recipients', account, backends=account.backend, caps=CapTransfer):
self.cached_format(recipient)
@contextmanager
def use_cmd_formatter(self, cmd):
self.set_formatter(self.commands_formatters.get(cmd, self.DEFAULT_FORMATTER))
try:
yield
finally:
self.flush()
def _build_transfer(self, line):
if self.interactive:
id_from, id_to, amount, reason, exec_date = self.parse_command_args(line, 5, 0)
else:
id_from, id_to, amount, reason, exec_date = self.parse_command_args(line, 5, 3)
missing = not bool(id_from and id_to and amount)
if id_from:
account = self.get_object(id_from, 'get_account', [])
id_from = account.id
if not account:
print('Error: account %s not found' % id_from, file=self.stderr)
return
else:
with self.use_cmd_formatter('list'):
self.do_ls('')
id_from = self.ask('Transfer money from account', default='')
if not id_from:
return
id_from, backend = self.parse_id(id_from)
account = find_object(self.objects, fullid='%s@%s' % (id_from, backend))
if not account:
return
id_from = account.id
if id_to:
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
rcpts = self.do('iter_transfer_recipients', id_from, backends=account.backend)
rcpt = find_object(rcpts, id=id_to)
else:
with self.use_cmd_formatter('recipients'):
self.do_recipients(account.fullid)
id_to = self.ask('Transfer money to recipient', default='')
if not id_to:
return
id_to, backend = self.parse_id(id_to)
rcpt = find_object(self.objects, fullid='%s@%s' % (id_to, backend))
if not rcpt:
return
if not amount:
amount = self.ask('Amount to transfer', default='', regexp=r'\d+(?:\.\d*)?')
try:
amount = Decimal(amount)
except (TypeError, ValueError, InvalidOperation):
print('Error: please give a decimal amount to transfer', file=self.stderr)
return
if amount <= 0:
print('Error: transfer amount must be strictly positive', file=self.stderr)
return
if missing:
reason = self.ask('Label of the transfer (seen by the recipient)', default='')
exec_date = self.ask('Execution date of the transfer (YYYY-MM-DD format, empty for today)', default='')
today = datetime.date.today()
if exec_date:
try:
exec_date = datetime.datetime.strptime(exec_date, '%Y-%m-%d').date()
except ValueError:
print('Error: execution date must be valid and in YYYY-MM-DD format', file=self.stderr)
return
if exec_date < today:
print('Error: execution date cannot be in the past', file=self.stderr)
return
else:
exec_date = today
transfer = Transfer()
transfer.backend = account.backend
transfer.account_id = account.id
transfer.account_label = account.label
transfer.account_iban = account.iban
transfer.recipient_id = id_to
if rcpt:
# Try to find the recipient label. It can be missing from
# recipients list, for example for banks which allow transfers to
# arbitrary recipients.
transfer.recipient_label = rcpt.label
transfer.recipient_iban = rcpt.iban
transfer.amount = amount
transfer.label = reason or u''
transfer.exec_date = exec_date
return transfer
def do_transfer(self, line):
"""
transfer [ACCOUNT RECIPIENT AMOUNT [LABEL [EXEC_DATE]]]
Make a transfer beetwen two accounts
- ACCOUNT the source account
- RECIPIENT the recipient
- AMOUNT amount to transfer
- LABEL label of transfer
- EXEC_DATE date when to execute the transfer
"""
transfer = self._build_transfer(line)
if transfer is None:
return 1
if self.interactive:
with self.use_cmd_formatter('transfer'):
self.start_format()
self.cached_format(transfer)
if not self.ask('Are you sure to do this transfer?', default=True):
return
# only keep basic fields because most modules don't handle others
transfer.account_label = None
transfer.account_iban = None
transfer.recipient_label = None
transfer.recipient_iban = None
self.start_format()
next(iter(self.do('transfer', transfer, backends=transfer.backend)))
def complete_transfer_history(self, text, line, *ignored):
return self.complete_history(self, text, line, *ignored)
@defaultcount(10)
def do_transfer_history(self, line):
"""
transfer_history [ACCOUNT_ID]
Display history of transfer transactions.
"""
id, = self.parse_command_args(line, 1, 0)
account = None
backends = None
if id:
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
backends = account.backend
self.start_format()
for tr in self.do('iter_transfers', account, backends=backends):
self.format(tr)
def show_wealth(self, command, id):
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
caps = {
'iter_investment': CapBankWealth,
'iter_pocket': CapBankWealth,
'iter_market_orders': CapBankWealth,
}
self.start_format()
for el in self.do(command, account, backends=account.backend, caps=caps[command]):
self.format(el)
def do_investment(self, id):
"""
investment ID
Display investments of an account.
"""
self.show_wealth('iter_investment', id)
def do_pocket(self, id):
"""
pocket ID
Display pockets of an account.
"""
self.show_wealth('iter_pocket', id)
@defaultcount(10)
def do_market_order(self, id):
"""
market_order ID
Display market orders of an account.
"""
self.show_wealth('iter_market_orders', id)
def do_budgea(self, line):
"""
budgea USERNAME PASSWORD
Export your bank accounts and transactions to Budgea.
Budgea is an online web and mobile application to manage your bank
accounts. To avoid giving your credentials to this service, you can use
this command.
https://www.budgea.com
"""
username, password = self.parse_command_args(line, 2, 2)
client = APIBrowser(baseurl='https://budgea.biapi.pro/2.0/',
proxy=getproxies(),
logger=getLogger('apibrowser', self.logger))
client.set_profile(Weboob(self.VERSION))
client.TIMEOUT = 60
try:
r = client.request('auth/token', data={'username': username, 'password': password, 'application': 'weboob'})
except BrowserHTTPError as r:
error = r.response.json()
print('Error: {}'.format(error.get('message', error['code'])), file=self.stderr)
return 1
client.session.headers['Authorization'] = 'Bearer %s' % r['token']
accounts = {}
for account in client.request('users/me/accounts')['accounts']:
if account['id_connection'] is None:
accounts[account['number']] = account
for account in self.do('iter_accounts'):
if account.id not in accounts:
r = client.request('users/me/accounts', data={'name': account.label,
'balance': account.balance,
'number': account.id,
})
self.logger.debug(r)
account_id = r['id']
else:
account_id = accounts[account.id]['id']
transactions = []
for tr in self.do('iter_history', account, backends=account.backend):
transactions.append({'original_wording': tr.raw,
'simplified_wording': tr.label,
'value': tr.amount,
'date': tr.date.strftime('%Y-%m-%d'),
})
r = client.request('users/me/accounts/%s/transactions' % account_id,
data={'transactions': transactions})
client.request('users/me/accounts/%s' % account_id, data={'balance': account.balance})
print('- %s (%s%s): %s new transactions' % (account.label, account.balance, account.currency_text, len(r)))
def do_profile(self, line):
"""
profile
Display detailed information about person or company.
"""
self.start_format()
for profile in self.do('get_profile', caps=CapProfile):
self.format(profile)
def do_emitters(self, line):
"""
emitters
Display transfer emitter account.
"""
self.objects = []
self.start_format()
for emitter in self.do('iter_emitters', backends=list(self.enabled_backends), caps=CapTransfer):
self.cached_format(emitter)
def main(self, argv):
self.load_config()
return super(Boobank, self).main(argv)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobathon/ 0000775 0000000 0000000 00000000000 13726205233 0026221 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobathon/__init__.py 0000664 0000000 0000000 00000001426 13726205233 0030335 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobathon import Boobathon
__all__ = ['Boobathon']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobathon/boobathon.py 0000664 0000000 0000000 00000065762 13726205233 0030566 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from datetime import datetime, timedelta
import re
import sys
from random import choice
from collections import OrderedDict
from weboob.capabilities.content import CapContent
from weboob.tools.application.repl import ReplApplication
from weboob.tools.compat import urlsplit
__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('Unable to parse h3=: %s' % line, file=self.stderr)
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('Unable to parse user "%s"' % line, file=self.stderr)
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('Unable to parse task: "%s"' % line, file=self.stderr)
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.values():
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2011-YEAR 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):
super(Boobathon, self).__init__(*args, **kwargs)
def main(self, argv):
if len(argv) < 2:
print('Please give the name of the boobathon', file=self.stderr)
return 1
self.event = Event(argv[1], choice(list(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.values():
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 range(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.values():
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.values()]
def do_close(self, name):
"""
close WINNER
Close the event and set the winner.
"""
self.event.load()
for member in self.event.members.values():
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('"%s" not found' % name, file=self.stderr)
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('Syntax: edit [event | me]', file=self.stderr)
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('You haven\'t joined the event.', file=self.stderr)
return 1
self.edit_member(mem)
self.save_event('Member edited')
else:
print('Unable to edit "%s"' % line, file=self.stderr)
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.values():
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('You have already joined this event.', file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
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('Unable to leave during the event, loser!', file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
try:
self.event.members.pop(self.event.backend.browser.get_userid())
except KeyError:
print("You have not joined this event.", file=self.stderr)
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("You have not joined this event.", file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
try:
task_id = int(line)
except ValueError:
print('The task ID should be a number', file=self.stderr)
return 2
try:
task = mem.tasks.pop(task_id)
except IndexError:
print('Unable to find task #%d' % task_id, file=self.stderr)
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("You have not joined this event.", file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
return 1
backend, capability = self.parse_command_args(line, 2, 2)
if not backend[0].isupper():
print('The backend name "%s" needs to start with a capital.' % backend, file=self.stderr)
return 2
if not capability.startswith('Cap') or not capability[3].isupper():
print('"%s" is not a proper capability name (must start with Cap).' % capability, file=self.stderr)
return 2
for task in mem.tasks:
if (task.backend,task.capability) == (backend,capability):
print("A task already exists for that.", file=self.stderr)
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("You have not joined this event.", file=self.stderr)
return 1
if len(mem.tasks) == 0:
print("You don't have any task to do.", file=self.stderr)
return 1
if not self.event.currently_in_event():
print("You can't start a task, we are not in event.", file=self.stderr)
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('Task not found.', file=self.stderr)
return 3
if task.status == task.STATUS_DONE:
print('Task is already done.', file=self.stderr)
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("You have not joined this event.", file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
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('Oops, you are out of event. Canceling the task...', file=self.stderr)
self.save_event('Cancel task')
return 1
return
print("There isn't any task in progress.", file=self.stderr)
return 1
def do_cancel(self, line):
"""
cancel
Cancel the current task.
"""
self.event.load()
mem = self.event.get_me()
if not mem:
print("You have not joined this event.", file=self.stderr)
return 1
if self.event.is_closed():
print("Boobathon is closed.", file=self.stderr)
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("There isn't any task in progress.", file=self.stderr)
return 1
def load_default_backends(self):
"""
Overload a Application method.
"""
for backend_name, module_name, params in self.weboob.backends_config.iter_backends():
if module_name != 'redmine':
continue
v = urlsplit(params['url'])
if v.netloc == 'symlink.me':
self.load_backends(names=[backend_name])
return
if not self.check_loaded_backends({'url': 'https://symlink.me'}):
print("Ok, so leave now.")
sys.exit(0)
def is_module_loadable(self, module):
"""
Overload a ConsoleApplication method.
"""
return module.name == 'redmine'
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobcoming/ 0000775 0000000 0000000 00000000000 13726205233 0026364 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobcoming/__init__.py 0000664 0000000 0000000 00000001423 13726205233 0030475 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobcoming import Boobcoming
__all__ = ['Boobcoming']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobcoming/boobcoming.py 0000664 0000000 0000000 00000040672 13726205233 0031065 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from datetime import time, datetime
from dateutil import tz
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, TICKET, STATUS
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.config.yamlconfig import YamlConfig
__all__ = ['Boobcoming']
class UpcomingSimpleFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'category', 'summary', 'status')
def format_obj(self, obj, alias):
result = u'%s - %s' % (obj.backend, obj.category)
if not empty(obj.start_date):
result += u' - %s' % obj.start_date.strftime('%H:%M')
result += u' - %s' % obj.summary
if obj.status == STATUS.CANCELLED:
result += u' (cancelled)'
return result
class ICalFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'status')
def start_format(self, **kwargs):
result = u'BEGIN:VCALENDAR\r\n'
result += u'VERSION:2.0\r\n'
result += u'PRODID:-//hacksw/handcal//NONSGML v1.0//EN\r\n'
self.output(result)
def format_obj(self, obj, alias):
result = u'BEGIN:VEVENT\r\n'
utc_zone = tz.gettz('UTC')
event_timezone = tz.gettz(obj.timezone)
start_date = obj.start_date if not empty(obj.start_date) else datetime.now()
if isinstance(start_date, datetime):
start_date = start_date.replace(tzinfo=event_timezone)
utc_start_date = start_date.astimezone(utc_zone)
result += u'DTSTART:%s\r\n' % utc_start_date.strftime("%Y%m%dT%H%M%SZ")
else:
result += u'DTSTART:%s\r\n' % start_date.strftime("%Y%m%d")
end_date = obj.end_date if not empty(obj.end_date) else datetime.combine(start_date, time.max)
if isinstance(end_date, datetime):
end_date = end_date.replace(tzinfo=event_timezone)
utc_end_date = end_date.astimezone(utc_zone)
result += u'DTEND:%s\r\n' % utc_end_date.strftime("%Y%m%dT%H%M%SZ")
else:
result += u'DTEND:%s\r\n' % end_date.strftime("%Y%m%d")
result += u'SUMMARY:%s\r\n' % obj.summary
result += u'UID:%s\r\n' % obj.id
result += u'STATUS:%s\r\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\r\n' % location
if hasattr(obj, 'categories') and not empty(obj.categories):
result += u'CATEGORIES:%s\r\n' % obj.categories
if hasattr(obj, 'description') and not empty(obj.description):
result += u'DESCRIPTION:%s\r\n' % obj.description.strip(' \t\n\r')\
.replace('\r', '')\
.replace('\n', r'\n')\
.replace(',', '\,')
if hasattr(obj, 'transp') and not empty(obj.transp):
result += u'TRANSP:%s\r\n' % obj.transp
if hasattr(obj, 'sequence') and not empty(obj.sequence):
result += u'SEQUENCE:%s\r\n' % obj.sequence
if hasattr(obj, 'url') and not empty(obj.url):
result += u'URL:%s\r\n' % obj.url
result += u'END:VEVENT\r\n'
return result
def flush(self, **kwargs):
self.output(u'END:VCALENDAR')
class UpcomingListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'category', 'status')
def get_title(self, obj):
return ' %s - %s ' % (obj.category, obj.summary)
def get_description(self, obj):
result = u''
if not empty(obj.start_date):
result += u'\tDate: %s\n' % obj.start_date.strftime('%A %d %B %Y')
result += u'\tHour: %s' % obj.start_date.strftime('%H:%M')
if not empty(obj.end_date):
result += ' - %s' % obj.end_date.strftime('%H:%M')
days_diff = (obj.end_date - obj.start_date).days
if days_diff >= 1:
result += ' (%i day(s) later)' % (days_diff)
result += '\n'
if obj.status == STATUS.CANCELLED:
result += '\tStatus: Cancelled\n'
return result.strip('\n\t')
class UpcomingFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'start_date', 'end_date', 'summary', 'category', 'status')
def format_obj(self, obj, alias):
result = u'%s%s - %s%s\n' % (self.BOLD, obj.category, obj.summary, self.NC)
if not empty(obj.start_date):
if not empty(obj.end_date):
days_diff = (obj.end_date - obj.start_date).days
if days_diff >= 1:
result += u'From: %s to %s ' % (obj.start_date.strftime('%A %d %B %Y'),
obj.end_date.strftime('%A %d %B %Y'))
else:
result += u'Date: %s\n' % obj.start_date.strftime('%A %d %B %Y')
result += u'Hour: %s' % obj.start_date.strftime('%H:%M')
result += ' - %s' % obj.end_date.strftime('%H:%M')
else:
result += u'Date: %s\n' % obj.start_date.strftime('%A %d %B %Y')
result += u'Hour: %s' % obj.start_date.strftime('%H:%M')
result += '\n'
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: %.2f\n' % obj.price
if hasattr(obj, 'ticket') and not empty(obj.ticket):
result += u'Ticket: %s\n' % obj.ticket
if hasattr(obj, 'url') and not empty(obj.url):
result += u'URL: %s\n' % obj.url
if hasattr(obj, 'status') and not empty(obj.status):
result += u'Status: %s\n' % obj.status
return result
class Boobcoming(ReplApplication):
APPNAME = 'boobcoming'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2012-YEAR 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',
'load': 'upcoming_list',
'ls': 'upcoming_list',
'info': 'upcoming',
'export': 'ical_formatter'
}
def main(self, argv):
self.load_config(klass=YamlConfig)
return ReplApplication.main(self, argv)
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)
def select_values(self, values_from, values_to, query_str):
r = 'notempty'
while r != '':
for i, value in enumerate(values_from, 1):
print(' %s%2d)%s [%s] %s' % (self.BOLD,
i,
self.NC,
'x' if value in values_to else ' ',
value))
r = self.ask(query_str, regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(values_from):
continue
value = list(values_from)[r - 1]
if value in values_to:
values_to.remove(value)
else:
values_to.append(value)
@defaultcount(10)
def do_search(self, line):
"""
search
search for an event. Parameters interactively asked
"""
query = Query()
self.select_values(CATEGORIES, query.categories, ' Select category (or empty to stop)')
self.select_values(TICKET, query.ticket, ' Select tickets status (or empty to stop)')
if query.categories and len(query.categories) > 0 and query.ticket and len(query.ticket) > 0:
query.city = self.ask('Enter a city', default='')
query.summary = self.ask('Enter a title', 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
save_query = self.ask('Save query (y/n)', default='n')
if save_query.upper() == 'Y':
name = ''
while not name:
name = self.ask('Query name')
self.config.set('queries', name, query)
self.config.save()
self.complete_search(query)
def complete_search(self, query):
self.change_path([u'events'])
self.start_format()
for 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_load(self, query_name):
"""
load [query name]
without query name : list loadable queries
with query name laod query
"""
queries = self.config.get('queries')
if not queries:
print('There is no saved queries', file=self.stderr)
return 2
if not query_name:
for name in queries.keys():
print(' %s* %s %s' % (self.BOLD,
self.NC,
name))
query_name = self.ask('Which one')
if query_name in queries:
self.complete_search(queries.get(query_name))
else:
print('Unknown query', file=self.stderr)
return 2
@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('Invalid argument: %s' % self.get_command_help('list'), file=self.stderr)
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 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('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
event = self.get_object(_id, 'get_event')
if not event:
print('Upcoming event not found: %s' % _id, file=self.stderr)
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('This command takes at leat one argument: %s' % self.get_command_help('export'), file=self.stderr)
return 2
_file, args = self.parse_command_args(line, 2, req_n=1)
l = self.retrieve_events(args)
if l == 3:
return 3
if not _file == "-":
dest = self.check_file_ext(_file)
self.formatter.outfile = dest
self.formatter.start_format()
for item in l:
self.format(item)
def retrieve_events(self, args):
l = []
if not args:
_ids = []
for 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('Upcoming event not found: %s' % _id, file=self.stderr)
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('This command takes at leat one argument: %s' % self.get_command_help('attends'), file=self.stderr)
return 2
args = self.parse_command_args(line, 1, req_n=1)
l = self.retrieve_events(args[0])
for event in l:
# we wait till the work be done, else the errors are not handled
self.do('attends_event', event, True).wait()
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('This command takes at leat one argument: %s' % self.get_command_help('unattends'), file=self.stderr)
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-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobill/ 0000775 0000000 0000000 00000000000 13726205233 0025670 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobill/__init__.py 0000664 0000000 0000000 00000001424 13726205233 0030002 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobill import Boobill
__all__ = ['Boobill']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobill/boobill.py 0000664 0000000 0000000 00000025237 13726205233 0027675 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from decimal import Decimal
import sys
from weboob.capabilities.bill import CapDocument, Detail, Subscription
from weboob.capabilities.profile import CapProfile
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.tools.application.captcha import CaptchaMixin
from weboob.core import CallErrors
from weboob.exceptions import CaptchaQuestion
from weboob.capabilities.captcha import exception_to_job
__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(CaptchaMixin, ReplApplication):
APPNAME = 'boobill'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2012-YEAR Florent Fourcot'
DESCRIPTION = 'Console application allowing to get/download documents and bills.'
SHORT_DESCRIPTION = "get/download documents and bills"
CAPS = CapDocument
COLLECTION_OBJECTS = (Subscription, )
EXTRA_FORMATTERS = {'subscriptions': SubscriptionsFormatter,
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'subscriptions': 'subscriptions',
'ls': 'subscriptions',
}
def load_default_backends(self):
self.load_backends(CapDocument, storage=self.create_storage())
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 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('Hint: There are more results available for %s (use option -n or count command)' % (', '.join(more_results)), file=self.stderr)
for backend in not_implemented:
print(u'Error(%s): This feature is not supported yet by this backend.' % backend.name, file=self.stderr)
def do_subscriptions(self, line):
"""
subscriptions
List all subscriptions.
"""
return self.do_ls(line)
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 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_documents(self, id):
"""
documents [ID]
Get the list of documents for subscriptions.
If no ID given, display documents of all backends
"""
self.exec_method(id, 'iter_documents')
@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, force_pdf=False):
"""
download [DOC_ID | all] [FILENAME]
download DOC_ID [FILENAME]
download the document
DOC_ID is the identifier of the document (hint: try documents command)
FILENAME is where to write the file. If FILENAME is '-',
the file is written to stdout.
download all [SUB_ID]
You can use special word "all" and download all documents of
subscription identified by SUB_ID.
If SUB_ID is not given, download documents of all subscriptions.
"""
id, dest = self.parse_command_args(line, 2, 1)
id, backend_name = self.parse_id(id)
if not id:
print('Error: please give a document ID (hint: use documents command)', file=self.stderr)
return 2
if id == 'all':
return self.download_all(dest, force_pdf)
names = (backend_name,) if backend_name is not None else None
document, = self.do('get_document', id, backends=names)
if not document:
print('Error: document not found')
return 1
if dest is None:
dest = id + "." + (document.format if not force_pdf else 'pdf')
for buf in self.do('download_document' if not force_pdf else 'download_document_pdf', document, backends=names):
if buf:
if dest == "-":
if sys.version_info.major >= 3:
self.stdout.buffer.write(buf)
else:
self.stdout.stream.write(buf)
else:
try:
with open(dest, 'wb') as f:
f.write(buf)
except IOError as e:
print('Unable to write document in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
def do_download_pdf(self, line):
"""
download_pdf [id | all]
download function with forced PDF conversion.
"""
return self.do_download(line, force_pdf=True)
def download_all(self, sub_id, force_pdf):
if sub_id:
sub_id, backend_name = self.parse_id(sub_id)
names = (backend_name,) if backend_name else None
subscription, = self.do('get_subscription', sub_id, backends=names)
if not self.download_subscription(subscription, force_pdf):
return 1
else:
for subscription in self.do('iter_subscription'):
if not self.download_subscription(subscription, force_pdf):
return 1
def download_subscription(self, subscription, force_pdf):
for document in self.do('iter_documents', subscription, backends=(subscription.backend,)):
if not self.download_doc(document, force_pdf):
return False
return True
def download_doc(self, document, force_pdf):
if force_pdf:
method = 'download_document_pdf'
else:
method = 'download_document'
dest = document.id + "." + (document.format if not force_pdf else 'pdf')
for buf in self.do(method, document, backends=(document.backend,)):
if buf:
try:
with open(dest, 'wb') as f:
f.write(buf)
except IOError as e:
print('Unable to write bill in "%s": %s' % (dest, e), file=self.stderr)
return False
return True
def do_profile(self, line):
"""
profile
Display detailed information about person or company.
"""
self.start_format()
for profile in self.do('get_profile', caps=CapProfile):
self.format(profile)
def bcall_error_handler(self, backend, error, backtrace):
"""
Handler for an exception inside the CallErrors exception.
This method can be overridden to support more exceptions types.
"""
if isinstance(error, CaptchaQuestion):
if not self.captcha_weboob.count_backends():
print('Error(%s): Site requires solving a CAPTCHA but no CapCaptchaSolver backends were configured' % backend.name,
file=self.stderr)
return False
print('Info(%s): Encountered CAPTCHA, please wait for its resolution, it can take dozens of seconds' % backend.name, file=self.stderr)
job = exception_to_job(error)
self.solve_captcha(job, backend)
return False
return super(ReplApplication, self).bcall_error_handler(backend, error, backtrace)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/booblyrics/ 0000775 0000000 0000000 00000000000 13726205233 0026415 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/booblyrics/__init__.py 0000664 0000000 0000000 00000001432 13726205233 0030526 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .booblyrics import Booblyrics
__all__ = ['Booblyrics']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/booblyrics/booblyrics.py 0000664 0000000 0000000 00000007037 13726205233 0031145 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2013-YEAR 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('Song lyrics not found: %s' % id, file=self.stderr)
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 songlyrics in self.do('iter_lyrics', criteria, pattern):
self.cached_format(songlyrics)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobmsg/ 0000775 0000000 0000000 00000000000 13726205233 0025676 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobmsg/__init__.py 0000664 0000000 0000000 00000001426 13726205233 0030012 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobmsg import Boobmsg
__all__ = ['Boobmsg']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobmsg/boobmsg.py 0000664 0000000 0000000 00000044342 13726205233 0027707 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import datetime
import hashlib
from tempfile import NamedTemporaryFile
from lxml import etree
from weboob.core import CallErrors
from weboob.capabilities.base import empty
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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 field in self.do('get_account_status',
backends=backend_name,
caps=CapAccount):
if field.backend in results:
results[field.backend].append(field)
else:
results[field.backend] = [field]
for name, fields in results.items():
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 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 msg in self.do(func):
self.format(msg)
def do_export_thread(self, arg):
"""
export_thread ID
Export the thread identified by ID
"""
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
cmd = self.do('get_thread', _id, backends=backend_name)
self.start_format()
for thread in cmd:
if thread is not None:
thread, = self.do('fillobj', thread, None, backends=thread.backend)
for msg in thread.iter_all_messages():
msg, = self.do('fillobj', msg, None, backends=thread.backend)
self.format(msg)
def do_show(self, arg):
"""
show MESSAGE
Read a message
"""
message = None
if len(arg) == 0:
print('Please give a message ID.', file=self.stderr)
return 2
try:
message = self.messages[int(arg) - 1]
except (IndexError, ValueError):
# The message is not is the cache, we have now two cases:
# 1) the user uses a number to get a thread in the cache
# 2) the user gives a thread id
try:
thread = self.threads[int(arg) - 1]
if not empty(thread.root):
message = thread.root
else:
for thread in self.do('get_thread', thread.id, backends=thread.backend):
if thread is not None:
if not thread.root:
thread, = self.do('fillobj', thread, ('root',), backends=thread.backend)
message = thread.root
except (IndexError, ValueError):
_id, backend_name = self.parse_id(arg)
for thread in self.do('get_thread', _id, backends=backend_name):
if thread is not None:
if not thread.root:
thread, = self.do('fillobj', thread, ('root',), backends=thread.backend)
message = thread.root
if not empty(message):
self.start_format()
self.format(message)
self.weboob.do('set_message_read', message, backends=message.backend)
return
else:
print('Message not found', file=self.stderr)
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 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("Configuration error: photo_viewer is undefined", file=self.stderr)
return
_id, backend_name = self.parse_id(id, unique_backend=True)
found = 0
for 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 = self.weboob[contact.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-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobooks/ 0000775 0000000 0000000 00000000000 13726205233 0026063 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobooks/__init__.py 0000664 0000000 0000000 00000001431 13726205233 0030173 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobooks import Boobooks
__all__ = ['Boobooks']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobooks/boobooks.py 0000664 0000000 0000000 00000005454 13726205233 0030262 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2012-YEAR 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',
'rented': '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('Error: please give a book ID (hint: use ls command)', file=self.stderr)
return 2
names = (backend_name,) if backend_name is not None else None
for renew in self.do('renew_book', id, backends=names):
self.format(renew)
def do_rented(self, args):
"""
rented
List rented books
"""
for book in self.do('iter_rented', backends=None):
self.format(book)
def do_search(self, pattern):
"""
search PATTERN
Search books
"""
for book in self.do('search_books', pattern):
self.format(book)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobsize/ 0000775 0000000 0000000 00000000000 13726205233 0026062 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobsize/__init__.py 0000664 0000000 0000000 00000001425 13726205233 0030175 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobsize import Boobsize
__all__ = ['Boobsize']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobsize/boobsize.py 0000664 0000000 0000000 00000015736 13726205233 0030264 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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
from weboob.tools.compat import unicode
__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 = '2.1'
COPYRIGHT = 'Copyright(C) 2013-YEAR 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('Error(%s): %s' % (backend.name, msg), file=self.stderr)
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 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 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 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 measure in self.do('get_last_measure', _id, backends=backend_name, caps=CapGauge):
self.format(measure)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobtracker/ 0000775 0000000 0000000 00000000000 13726205233 0026543 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobtracker/__init__.py 0000664 0000000 0000000 00000001433 13726205233 0030655 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boobtracker import BoobTracker
__all__ = ['BoobTracker']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boobtracker/boobtracker.py 0000664 0000000 0000000 00000043020 13726205233 0031411 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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.compat import basestring, unicode
from weboob.tools.html import html2text
from weboob.tools.date import parse_french_date
__all__ = ['BoobTracker']
try:
input = raw_input
except NameError:
pass
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.items():
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2011-YEAR 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('Please enter a project name', file=self.stderr)
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 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('This command takes an argument: %s' % self.get_command_help('get', short=True), file=self.stderr)
return 2
issue = self.get_object(line, 'get_issue')
if not issue:
print('Issue not found: %s' % line, file=self.stderr)
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('Error: HOURS parameter may be a float', file=self.stderr)
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.items():
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
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
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 list(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('Issue not found: %s' % _id, file=self.stderr)
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('Not implemented yet.', file=self.stderr)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boomoney/ 0000775 0000000 0000000 00000000000 13726205233 0026075 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boomoney/__init__.py 0000664 0000000 0000000 00000001414 13726205233 0030206 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Written by Bruno Chabrier
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boomoney import Boomoney
__all__ = ['Boomoney']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boomoney/boomoney.py 0000664 0000000 0000000 00000052245 13726205233 0030306 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
# start with:
# set PYTHONPATH=D:\Dropbox\Projets\boomoney
# python d:\Dropbox\Projets\boomoney\scripts\boomoney -N
from threading import Thread, Lock
import copy
import sys
from io import StringIO
import os
import re
import subprocess
import datetime
from optparse import OptionGroup
import shutil
from colorama import init, Fore, Style
from weboob.tools.compat import unicode
from weboob.exceptions import BrowserUnavailable
from weboob.capabilities.bank import AccountNotFound, AccountType
from weboob.applications.boobank import Boobank
from weboob.applications.boobank.boobank import OfxFormatter
from weboob.tools.application.formatters.simple import SimpleFormatter
__all__ = ['Boomoney']
printMutex = Lock()
numMutex = Lock()
backupMutex = Lock()
class MoneyOfxFormatter(OfxFormatter):
def start_format(self, **kwargs):
self.seen = set()
# MSMoney only supports CHECKING accounts
t = kwargs['account'].type
kwargs['account'].type = AccountType.CHECKING
super(MoneyOfxFormatter, self).start_format(**kwargs)
kwargs['account'].type = t
def format_obj(self, obj, alias):
cat = obj.category
obj.category = obj.raw
result = super(MoneyOfxFormatter, self).format_obj(obj, alias)
obj.category = cat
return result
def output(self, formatted):
if self.outfile != sys.stdout:
self.outfile.write(formatted + os.linesep)
else:
super(MoneyOfxFormatter, self).output(formatted)
class ListFormatter(SimpleFormatter):
def output(self, formatted):
if self.outfile != sys.stdout:
self.outfile.write(formatted + os.linesep)
else:
super(ListFormatter, self).output(formatted)
class BoobankNoBackend(Boobank):
EXTRA_FORMATTERS = {'ops_list': MoneyOfxFormatter}
COMMANDS_FORMATTERS = {'history': 'ops_list'}
def load_default_backends(self):
pass
def bcall_error_handler(self, backend, error, backtrace):
handled = False
if isinstance(error, BrowserUnavailable):
handled = True
self.error = True
if isinstance(error, AccountNotFound):
handled = True
self.error = True
if isinstance(error, NotImplementedError):
handled = True
self.error = False
if not handled:
self.error = True
self.weboob.logger.error("Unsupported error %s in BoobankNoBackend" % type(error))
return super(Boobank, self).bcall_error_handler(backend, error, backtrace)
class HistoryThread(Thread):
def __init__(self, boomoney, account):
Thread.__init__(self)
self.boomoney = boomoney
self.account = account
self.disabled = boomoney.config.get(account, 'disabled', default=False)
self.date_min = boomoney.config.get(account, 'date_min', default='')
self.last_date = boomoney.config.get(account, 'last_date', default='')
@property
def label(self):
if self.account in self.boomoney.labels:
return self.boomoney.labels[self.account]
else:
return self.boomoney.config.get(self.account, 'label', default='')
def dumpTransaction(self, output, fields, field):
output.write("\n")
if "DTUSER" in field and "DTPOSTED" in field and not field["DTUSER"] == field["DTPOSTED"]:
# the payment date is a deferred payment
# MSMoney takes DTPOSTED, which is the payment date
# I prefer to have the date of the operation, so I set DTPOSTED
# as DTUSER
field["DTPOSTED"] = field["DTUSER"]
for f in fields.strip().split(" "):
value = field[f]
if f == "NAME":
if value == "":
# MSMoney does not support empty NAME field
value = ""
else:
# MSMoney does not support NAME field longer than 64
value = value[:64]
output.write("<%s>%s\n" % (f, value))
output.write("\n")
def run(self):
now = datetime.datetime.now().strftime("%Y-%m-%d")
if self.boomoney.options.force:
from_date = self.date_min
else:
from_date = self.last_date
if from_date >= now:
self.boomoney.print(Style.BRIGHT + "%s (%s): Last import date is %s, no need to import again..." % (
self.account, self.label, self.last_date) + Style.RESET_ALL)
return
boobank = self.boomoney.createBoobank(self.account)
if boobank is None:
with numMutex:
self.boomoney.importIndex = self.boomoney.importIndex + 1
return
boobank.stderr = StringIO()
boobank.stdout = boobank.stderr
id, backend = self.account.split("@")
module_name, foo = boobank.weboob.backends_config.get_backend(backend)
moduleHandler = "%s.bat" % os.path.join(os.path.dirname(self.boomoney.getMoneyFile()), module_name)
self.boomoney.logger.info("Starting history of %s (%s)..." % (self.account, self.label))
MAX_RETRIES = 3
count = 0
found = False
content = ''
boobank.error = False
while count <= MAX_RETRIES and not (found and not boobank.error):
boobank.options.outfile = StringIO()
boobank.error = False
# executing history command
boobank.onecmd("history " + self.account + " " + from_date)
if count > 0:
self.boomoney.logger.info("Retrying %s (%s)... %i/%i" % (self.account, self.label, count, MAX_RETRIES))
found = re.match(r'^OFXHEADER:100', boobank.options.outfile.getvalue())
if found and not boobank.error:
content = boobank.options.outfile.getvalue()
boobank.options.outfile.close()
count = count + 1
if content == '':
# error occurred
with numMutex:
self.boomoney.importIndex = self.boomoney.importIndex + 1
index = self.boomoney.importIndex
self.boomoney.logger.error("(%i/%i) %s (%s): %saborting after %i retries.%s" % (
index, len(self.boomoney.threads),
self.account,
self.label,
Fore.RED + Style.BRIGHT,
MAX_RETRIES,
Style.RESET_ALL))
return
# postprocessing of the ofx content to match MSMoney expectations
content = re.sub(r'Not loaded', r'', content)
input = StringIO(content)
output = StringIO()
field = {}
fields = ' '
for line in input:
if re.match(r'^OFXHEADER:100', line):
inTransaction = False
if re.match(r'^', line):
inTransaction = True
if not inTransaction:
output.write(line)
if re.match(r'^', line):
# MSMoney expects CHECKNUM instead of NAME for CHECK transactions
if "TRNTYPE" in field and field["TRNTYPE"] == "CHECK":
if "NAME" in field and unicode(field["NAME"]).isnumeric():
field["CHECKNUM"] = field["NAME"]
del field["NAME"]
fields = fields.replace(' NAME ', ' CHECKNUM ')
# go through specific backend process if any
IGNORE = False
NEW = None
origfields = fields
origfield = field.copy()
if os.path.exists(moduleHandler):
self.boomoney.logger.info("Calling backend handler %s..." % moduleHandler)
# apply the transformations, in the form
# field_NAME=...
# field_MEMO=...
# field=...
cmd = 'cmd /C '
for f in field:
value = field[f]
cmd = cmd + 'set field_%s=%s& ' % (f, value)
cmd = cmd + '"' + moduleHandler + '"'
result = subprocess.check_output(cmd.encode(sys.stdout.encoding))
for line in re.split(r'[\r\n]+', result):
if not line == "":
f, value = line.split("=", 1)
if f == "IGNORE":
IGNORE = True
elif f == "NEW":
NEW = value
elif f.startswith('field_'):
f = re.sub(r'^field_', '', f)
if value == "":
if f in field:
del field[f]
fields = re.sub(" " + f + " ", " ", fields)
else:
field[f] = value
if f not in fields.strip().split(" "):
# MSMoney does not like when CHECKNUM is after MEMO
if f == "CHECKNUM":
fields = fields.replace("MEMO", "CHECKNUM MEMO")
else:
fields = fields + f + " "
if not IGNORE:
# dump transaction
self.dumpTransaction(output, fields, field)
if NEW is not None:
for n in NEW.strip().split(" "):
fields = origfields
field = origfield.copy()
field["FITID"] = origfield["FITID"] + "_" + n
for line in re.split(r'[\r\n]+', result):
if not line == "":
f, value = line.split("=", 1)
if f.startswith(n + '_field_'):
f = re.sub(r'^.*_field_', '', f)
field[f] = value
if f not in fields.strip().split(" "):
fields = fields + f + " "
# dump secondary transaction
self.dumpTransaction(output, fields, field)
inTransaction = False
if inTransaction:
if re.match(r'^', line):
field = {}
fields = ' '
else:
t = line.split(">", 1)
v = re.split(r'[\r\n]', t[1])
field[t[0][1:]] = v[0]
fields = fields + t[0][1:] + ' '
ofxcontent = output.getvalue()
stderrcontent = boobank.stderr.getvalue()
input.close()
output.close()
boobank.stderr.close()
if self.boomoney.options.display:
self.boomoney.print(Style.BRIGHT + ofxcontent + Style.RESET_ALL)
nbTransactions = ofxcontent.count('')
# create ofx file
fname = re.sub(r'[^\w@\. ]', '_', self.account + " " + self.label)
ofxfile = os.path.join(self.boomoney.getDownloadsPath(), fname + ".ofx")
with open(ofxfile, "w") as ofx_file:
ofx_file.write(re.sub(r'\r\n', r'\n', ofxcontent.encode(sys.stdout.encoding)))
with numMutex:
self.boomoney.write(stderrcontent)
self.boomoney.importIndex = self.boomoney.importIndex + 1
index = self.boomoney.importIndex
if not (self.boomoney.options.noimport or nbTransactions == 0):
self.boomoney.backupIfNeeded()
with printMutex:
if self.boomoney.options.noimport or nbTransactions == 0:
if nbTransactions == 0:
print(Style.BRIGHT + '(%i/%i) %s (%s) (no transaction).' % (
index, len(self.boomoney.threads),
self.account,
self.label
) + Style.RESET_ALL)
else:
print(Fore.GREEN + Style.BRIGHT + '(%i/%i) %s (%s) (%i transaction(s)).' % (
index, len(self.boomoney.threads),
self.account,
self.label,
nbTransactions
) + Style.RESET_ALL)
else:
# import into money
print(Fore.GREEN + Style.BRIGHT + '(%i/%i) Importing "%s" into MSMoney (%i transaction(s))...' % (
index, len(self.boomoney.threads),
ofxfile,
nbTransactions
) + Style.RESET_ALL)
if not self.boomoney.options.noimport:
if nbTransactions > 0:
subprocess.check_call('"%s" %s' % (
os.path.join(self.boomoney.getMoneyPath(), "mnyimprt.exe"),
ofxfile))
self.last_date = now
class Boomoney(Boobank):
APPNAME = 'boomoney'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2018-YEAR Bruno Chabrier'
DESCRIPTION = "Console application that imports bank accounts into Microsoft Money"
SHORT_DESCRIPTION = "import bank accounts into Microsoft Money"
EXTRA_FORMATTERS = {'list': ListFormatter}
COMMANDS_FORMATTERS = {'list': 'list'}
def __init__(self):
super(Boobank, self).__init__()
self.importIndex = 0
application_options = OptionGroup(self._parser, 'Boomoney Options')
application_options.add_option('-F', '--force', action='store_true', help='forces the retrieval of transactions (10 maximum), otherwise retrieves only the transactions newer than the previous retrieval date')
application_options.add_option('-A', '--account', help='retrieves only the specified account. By default, all accounts are retrieved')
application_options.add_option('-N', '--noimport', action='store_true', help='no import. Generates the files, but they are not imported in MSMoney. Last import dates are not modified')
application_options.add_option('-D', '--display', action='store_true', help='displays the generated OFX file')
application_options.add_option('-P', '--parallel', action='store_true', help='retrieves all accounts in parallel instead of one by one (experimental)')
self._parser.add_option_group(application_options)
self.labels = dict()
def print(self, *args):
with printMutex:
print(*args)
def write(self, *args):
with printMutex:
sys.stdout.write(*args)
def createBoobank(self, account):
accountId, backendName = account.split("@")
if not self.weboob.backends_config.backend_exists(backendName):
self.logger.warning("Unknown backend '%s' of account '%s' (not found in backends)" % (backendName, account))
return None
# create a Boobank instance
boobank = BoobankNoBackend()
boobank.options = copy.copy(self.options)
moduleName = self.weboob.backends_config._read_config().get(backendName, "_module")
module = self.weboob.modules_loader.loaded[moduleName]
backend = self.weboob.backend_instances[backendName]
params = {}
for param in backend.config:
params[param] = backend.config[param].get()
dedicatedBackendInstanceName = "backend instance for " + account
boobank.APP_NAME = "boobank app for " + account
instance = module.create_instance(self.weboob, dedicatedBackendInstanceName, params, storage=boobank.create_storage())
boobank.enabled_backends = set()
boobank.enabled_backends.add(instance)
boobank.weboob.backend_instances[dedicatedBackendInstanceName] = instance
boobank.selected_fields = ["$full"]
boobank.formatter = self.formatter
boobank._interactive = False
return boobank
def getHistory(self, account):
t = HistoryThread(self, account)
return t
def getDownloadsPath(self):
if not hasattr(self, '_downloadsPath'):
s = subprocess.check_output(
'reg query "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders" /v "{374DE290-123F-4565-9164-39C4925E467B}"')
t = re.sub(r'^(.|\r|\n)+REG_EXPAND_SZ\s+([^\n\r]+)(.|\r|\n)*$', r'\2', s)
self._downloadsPath = os.path.expandvars(t).decode('CP850')
return self._downloadsPath
def getMoneyPath(self):
if not hasattr(self, '_moneyPath'):
s = subprocess.check_output('reg query HKEY_CLASSES_ROOT\\money\\Shell\\Open\\Command /ve')
t = re.sub(r'^(.|\r|\n)+REG_SZ\s+([^\n\r]+)(.|\r|\n)*$', r'\2', s)
self._moneyPath = os.path.expandvars(os.path.dirname(t)).decode('CP850')
return self._moneyPath
def getMoneyFile(self):
if not hasattr(self, '_moneyFile'):
s = subprocess.check_output('reg query HKEY_CURRENT_USER\\Software\\Microsoft\\Money\\14.0 /v CurrentFile')
t = re.sub(r'^(.|\r|\n)+REG_SZ\s+([^\n\r]+)(.|\r|\n)*$', r'\2', s)
self._moneyFile = os.path.expandvars(t).decode('CP850')
return self._moneyFile
def backupIfNeeded(self):
if not (hasattr(self, '_backupDone') and self._backupDone):
with backupMutex:
# redo the test in mutual exclusion
if not (hasattr(self, '_backupDone') and self._backupDone):
file = self.getMoneyFile()
filename = os.path.splitext(os.path.basename(file))[0]
dir = os.path.dirname(file)
self.print(Fore.YELLOW + Style.BRIGHT + "Creating backup of %s..." % file + Style.RESET_ALL)
target = os.path.join(dir, filename + datetime.datetime.now().strftime("_%Y_%m_%d_%H%M%S.mny"))
shutil.copy2(file, target)
self._backupDone = True
def save_config(self):
for t in self.threads:
self.config.set(t.account, 'label', t.label)
self.config.set(t.account, 'disabled', t.disabled)
self.config.set(t.account, 'date_min', t.date_min)
self.config.set(t.account, 'last_date', t.last_date)
self.config.save()
def getList(self):
self.onecmd("select id label")
self.options.outfile = StringIO()
self.onecmd("list")
listContent = self.options.outfile.getvalue()
self.options.outfile.close()
self.print(Style.BRIGHT + "Accounts:%s----------%s%s----------" % (
os.linesep,
os.linesep,
listContent) + Style.RESET_ALL)
for line in listContent.split(os.linesep):
if not line == "":
idspec, labelspec = line.split("\t")
notusedid, id = idspec.split("=")
notusedlabel, label = labelspec.split("=")
self.labels[id] = label
def checkNew(self):
new = set()
for account in self.labels:
if account not in self.config.config.sections():
new.add(HistoryThread(self, account))
return new
def main(self, argv):
init()
self.load_config()
self._interactive = False
self.threads = set()
self.logger.info(self.config.config.sections())
for account in self.config.config.sections():
if self.options.account == None or account == self.options.account:
if self.config.config.getboolean(account, "disabled") == False:
# time.sleep(3)
self.threads.add(self.getHistory(account))
if self.options.parallel:
self.print(Fore.MAGENTA + Style.BRIGHT + "Starting %i history threads..." % len(self.threads) + Style.RESET_ALL)
for t in self.threads:
t.start()
self.getList()
for t in self.checkNew():
t.start()
self.threads.add(t)
self.print(Fore.MAGENTA + Style.BRIGHT + "Waiting for %i threads to complete..." % len(self.threads) + Style.RESET_ALL)
for t in self.threads:
t.join()
else:
self.getList()
for t in self.checkNew():
self.threads.add(t)
for t in self.threads:
t.start()
t.join()
self.save_config()
return
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boorpg/ 0000775 0000000 0000000 00000000000 13726205233 0025536 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boorpg/__init__.py 0000664 0000000 0000000 00000001423 13726205233 0027647 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .boorpg import BooRPG
__all__ = ['BooRPG']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/boorpg/boorpg.py 0000664 0000000 0000000 00000007571 13726205233 0027412 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2019-2020 Célande Adrien
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from weboob.capabilities.rpg import CapRPG, Character, Skill, CharacterClass, CollectableItem
from weboob.tools.application.repl import ReplApplication, defaultcount
class BooRPG(ReplApplication):
APPNAME = 'boorpg'
VERSION = '1.0'
COPYRIGHT = 'Copyright(C) 2019-YEAR Célande Adrien'
CAPS = CapRPG
DESCRIPTION = 'Console application allowing to list informations from a RPG.'
SHORT_DESCRIPTION = 'Manage RPG data'
DEFAULT_FORMATTER = 'table'
COLLECTION_OBJECTS = (Character, Skill, CharacterClass, CollectableItem, )
def do_characters(self, line):
"""
characters
List all characters
"""
print('do characterS')
print(f'line: "{line}"')
return self.do_ls(line)
def do_character(self, line):
"""
character ID
Get data on one character
"""
character_id, = self.parse_command_args(line, 1, 1)
self.start_format()
# cannot use get_object because it can be skipped after a ls
for c in self.do('get_character', character_id):
self.format(c)
@defaultcount(20)
def do_skills(self, line):
"""
skills [TYPE]
List all skills
"""
print('do skills')
skill_type, = self.parse_command_args(line, 1, 0)
print('skill type', skill_type)
self.start_format()
for skill in self.do('iter_skills', skill_type):
self.format(skill)
def do_skill(self, line):
"""
skill ID
Details for one skill
"""
skill_id, = self.parse_command_args(line, 1, 1)
self.start_format()
skill = self.get_object(skill_id, 'get_skill', [])
self.format(skill)
@defaultcount(20)
def do_skill_set(self, line):
"""
skill_set CHARACTER_ID [TYPE]
List of skills for a character
"""
character_id, skill_type = self.parse_command_args(line, 2, 1)
self.start_format()
for skill in self.do('iter_skill_set', character_id, skill_type):
self.format(skill)
@defaultcount(20)
def do_classes(self, line):
"""
classes
List all character classes
"""
self.start_format()
for character_class in self.do('iter_character_classes'):
self.format(character_class)
def do_class(self, line):
"""
class ID
Details for one character class
"""
class_id, = self.parse_command_args(line, 1, 1)
self.start_format()
character_class = self.get_object(class_id, 'get_character_class', [])
self.format(character_class)
@defaultcount(20)
def do_items(self, line):
"""
items
List all collectable items
"""
self.start_format()
for item in self.do('iter_collectable_items'):
self.format(item)
def do_item(self, line):
"""
item ID
Details for one collectable item
"""
item_id, = self.parse_command_args(line, 1, 1)
self.start_format()
item = self.get_object(item_id, 'get_collectable_item', [])
self.format(item)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/cineoob/ 0000775 0000000 0000000 00000000000 13726205233 0025664 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/cineoob/__init__.py 0000664 0000000 0000000 00000001421 13726205233 0027773 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .cineoob import Cineoob
__all__ = ['Cineoob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/cineoob/cineoob.py 0000664 0000000 0000000 00000064011 13726205233 0027656 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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, NotAvailable
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 person in lpersons:
result += ' * %s\n' % person[1]
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[1]
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2013-YEAR 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('Person not found: %s' % id1, file=self.stderr)
return 3
person2 = self.get_object(id2, 'get_person', caps=CapCinema)
if not person2:
print('Person not found: %s' % id2, file=self.stderr)
return 3
initial_count = self.options.count
self.options.count = None
lid1 = []
for id in self.do('iter_person_movies_ids', person1.id, caps=CapCinema):
lid1.append(id)
lid2 = []
for 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))
chrono_list = []
for common in inter:
movie = self.get_object(common, 'get_movie', caps=CapCinema)
role1 = movie.get_roles_by_person_id(person1.id)
if not role1:
role1 = movie.get_roles_by_person_name(person1.name)
role2 = movie.get_roles_by_person_id(person2.id)
if not role2:
role2 = movie.get_roles_by_person_name(person2.name)
if (movie.release_date != NotAvailable):
year = movie.release_date.year
else:
year = '????'
movie.short_description = '(%s) %s as %s ; %s as %s'%(year, person1.name, ', '.join(role1), person2.name, ', '.join(role2))
if movie:
i = 0
while (i chrono_list[i].release_date.year)):
i += 1
chrono_list.insert(i, movie)
for movie in chrono_list:
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('Movie not found: %s' % id1, file=self.stderr)
return 3
movie2 = self.get_object(id2, 'get_movie', caps=CapCinema)
if not movie2:
print('Movie not found: %s' % id2, file=self.stderr)
return 3
initial_count = self.options.count
self.options.count = None
lid1 = []
for id in self.do('iter_movie_persons_ids', movie1.id, caps=CapCinema):
lid1.append(id)
lid2 = []
for 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)
role1 = person.get_roles_by_movie_id(movie1.id)
if not role1:
role1 = person.get_roles_by_movie_title(movie1.original_title)
role2 = person.get_roles_by_movie_id(movie2.id)
if not role2:
role2 = person.get_roles_by_movie_title(movie2.original_title)
person.short_description = '%s in %s ; %s in %s'%(', '.join(role1), movie1.original_title, ', '.join(role2), movie2.original_title)
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('Movie not found: %s' % id, file=self.stderr)
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('Person not found: %s' % id, file=self.stderr)
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 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 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('Movie not found: %s' % id, file=self.stderr)
return 3
for 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('Person not found: %s' % id, file=self.stderr)
return 3
for 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('Person not found: %s' % person_id, file=self.stderr)
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('Movie not found: %s' % id, file=self.stderr)
return 3
# i would like to clarify with fillobj but how could i fill the movie AND choose the country ?
for 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('Movie releases not found for %s' % movie.original_title, file=self.stderr)
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('Torrent not found: %s' % id, file=self.stderr)
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 buf in self.do('get_torrent_file', _id, backends=backend_name, caps=CapTorrent):
if buf:
if dest == '-':
print(buf)
else:
try:
with open(dest, 'wb') as f:
f.write(buf)
except IOError as e:
print('Unable to write .torrent in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, MagnetOnly):
print(u'Error(%s): No direct URL available, '
u'please provide this magnet URL '
u'to your client:\n%s' % (backend, error.magnet), file=self.stderr)
return 4
else:
self.bcall_error_handler(backend, error, backtrace)
print('Torrent "%s" not found' % id, file=self.stderr)
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 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('Movie not found: %s' % id, file=self.stderr)
return 3
pattern = movie.original_title
self.change_path([u'search torrent'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for 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('Subtitle not found: %s' % id, file=self.stderr)
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 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('Unable to write file in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
print('Subtitle "%s" not found' % id, file=self.stderr)
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 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('Movie not found: %s' % id, file=self.stderr)
return 3
pattern = movie.original_title
self.change_path([u'search subtitle'])
if not pattern:
pattern = None
self.start_format(pattern=pattern)
for subtitle in self.do('iter_subtitles', language=language, pattern=pattern, caps=CapSubtitle):
self.cached_format(subtitle)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/comparoob/ 0000775 0000000 0000000 00000000000 13726205233 0026227 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/comparoob/__init__.py 0000664 0000000 0000000 00000000123 13726205233 0030334 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
from .comparoob import Comparoob
__all__ = ['Comparoob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/comparoob/comparoob.py 0000664 0000000 0000000 00000014047 13726205233 0030570 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.pricecomparison import CapPriceComparison
from weboob.tools.html import html2text
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter, PrettyFormatter
from weboob.tools.application.base import MoreResultsAvailable
from weboob.tools.application.console import ConsoleApplication
__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 = '2.1'
COPYRIGHT = 'Copyright(C) 2012-YEAR 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
@defaultcount(10)
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 product in self.do('search_products', pattern):
double = False
for prod in products.keys():
if product.name == prod:
double = True
products[product.name].append(product)
break
if not double:
products[product.name] = [product]
products_type = None
products_names = list(products.keys())
if len(products_names) == 0:
print('Error: no product found with this pattern', file=self.stderr)
return 1
elif len(products_names) == 1:
products_type = products_names[0]
else:
print('What product do you want to compare?')
for i, p in enumerate(products_names):
print(' %s%2d)%s %s' % (self.BOLD, i+1, self.NC, p))
r = int(self.ask(' Select a product', regexp='\d+'))
while products_type is None:
if r <= 0 or r > len(products):
print('Error: Please enter a valid ID')
continue
products_type = products_names[r-1]
self.change_path([u'prices'])
_products = self.get_object_list('iter_prices', products.get(products_type))
self._sort_display_products(_products)
def _sort_display_products(self, products):
if products:
self.start_format()
for price in sorted(products, key=self._get_price):
self.cached_format(price)
def bcall_errors_handler(self, errors, debugmsg='Use --debug option to print backtraces', ignore=()):
for backend, error, backtrace in errors.errors:
if isinstance(error, MoreResultsAvailable):
products = self.get_object_list()
self._sort_display_products(products)
ConsoleApplication.bcall_errors_handler(self, errors, debugmsg, ignore)
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('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
price = self.get_object(_id, 'get_price')
if not price:
print('Price not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(price)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/cookboob/ 0000775 0000000 0000000 00000000000 13726205233 0026043 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/cookboob/__init__.py 0000664 0000000 0000000 00000001423 13726205233 0030154 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .cookboob import Cookboob
__all__ = ['Cookboob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/cookboob/cookboob.py 0000664 0000000 0000000 00000012610 13726205233 0030212 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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
from weboob.tools.capabilities.recipe import recipe_to_krecipes_xml
__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 += u' * %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 = '2.1'
COPYRIGHT = 'Copyright(C) 2013-YEAR 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('Recipe not found: %s' % id, file=self.stderr)
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_to_krecipes_xml(recipe, 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('Unable to write .kreml in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
print('Recipe "%s" not found' % id, file=self.stderr)
return 3
@defaultcount(10)
def do_search(self, pattern):
"""
search [PATTERN]
Search recipes.
"""
self.change_path([u'search'])
self.start_format(pattern=pattern)
for recipe in self.do('iter_recipes', pattern=pattern):
self.cached_format(recipe)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/flatboob/ 0000775 0000000 0000000 00000000000 13726205233 0026036 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/flatboob/__init__.py 0000664 0000000 0000000 00000000067 13726205233 0030152 0 ustar 00root root 0000000 0000000 from .flatboob import Flatboob
__all__ = ['Flatboob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/flatboob/flatboob.py 0000664 0000000 0000000 00000024143 13726205233 0030204 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.housing import (CapHousing, Query, POSTS_TYPES,
ADVERT_TYPES, HOUSE_TYPES)
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.tools.config.yamlconfig import YamlConfig
__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
if hasattr(obj, 'url') and obj.url:
result += 'URL: %s\n' % obj.url
result += 'Cost: %s%s %s\n' % (obj.cost, obj.currency, obj.utilities)
area = u'%.2fm²' % (obj.area) if obj.area else u'%s' % obj.area
result += u'Area: %s\n' % area
if hasattr(obj, 'price_per_meter') and not empty(obj.price_per_meter):
result += u'Price per square meter: %.2f %s/m²\n' % (obj.price_per_meter, obj.currency)
if hasattr(obj, 'rooms') and not empty(obj.rooms):
result += u'Rooms: %d\n' % (obj.rooms)
if hasattr(obj, 'bedrooms') and not empty(obj.bedrooms):
result += u'Bedrooms: %d\n' % (obj.bedrooms)
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.items():
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 - %s' % (obj.cost, obj.currency, obj.utilities, 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 = '2.1'
COPYRIGHT = 'Copyright(C) 2012-YEAR Romain Bignon'
DESCRIPTION = "Console application to search for housing."
SHORT_DESCRIPTION = "search for housing"
CAPS = CapHousing
CONFIG = {'queries': {}}
EXTRA_FORMATTERS = {'housing_list': HousingListFormatter,
'housing': HousingFormatter,
}
COMMANDS_FORMATTERS = {'search': 'housing_list',
'info': 'housing',
'load': 'housing_list'
}
def main(self, argv):
self.load_config(klass=YamlConfig)
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 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 (%s)' % (self.BOLD, i+1, self.NC, 'x' if city in query.cities else ' ', city.name, city.backend))
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 i, good in enumerate(HOUSE_TYPES, 1):
print(' %s%2d)%s [%s] %s' % (self.BOLD,
i,
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(HOUSE_TYPES):
continue
value = list(HOUSE_TYPES)[r - 1]
if value in query.house_types:
query.house_types.remove(value)
else:
query.house_types.append(value)
_type = None
posts_types = sorted(POSTS_TYPES)
while _type not in range(len(posts_types)):
for i, t in enumerate(posts_types):
print(' %s%2d)%s %s' % (self.BOLD,
i,
self.NC,
t))
_type = self.ask_int('Type of query')
query.type = posts_types[_type]
r = 'notempty'
while r != '':
for i, good in enumerate(ADVERT_TYPES, 1):
print(' %s%2d)%s [%s] %s' % (self.BOLD,
i,
self.NC,
'x' if good in query.advert_types else ' ', good))
r = self.ask('Select type of posts (or empty to stop)', regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(ADVERT_TYPES):
continue
value = list(ADVERT_TYPES)[r - 1]
if value in query.advert_types:
query.advert_types.remove(value)
else:
query.advert_types.append(value)
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')
save_query = self.ask('Save query (y/n)', default='n')
if save_query.upper() == 'Y':
name = ''
while not name:
name = self.ask('Query name')
self.config.set('queries', name, query)
self.config.save()
self.complete_search(query)
def complete_search(self, query):
self.change_path([u'housings'])
self.start_format()
for 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
@defaultcount(10)
def do_load(self, query_name):
"""
load [query name]
without query name : list loadable queries
with query name laod query
"""
queries = self.config.get('queries')
if not queries:
print('There is no saved queries', file=self.stderr)
return 2
if not query_name:
for name in queries.keys():
print(' %s* %s %s' % (self.BOLD,
self.NC,
name))
query_name = self.ask('Which one')
if query_name in queries:
self.complete_search(queries.get(query_name))
else:
print('Unknown query', file=self.stderr)
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 housing.
"""
if not _id:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
housing = self.get_object(_id, 'get_housing')
if not housing:
print('Housing not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(housing)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/galleroob/ 0000775 0000000 0000000 00000000000 13726205233 0026214 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/galleroob/__init__.py 0000664 0000000 0000000 00000001434 13726205233 0030327 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .galleroob import Galleroob
__all__ = ['Galleroob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/galleroob/galleroob.py 0000664 0000000 0000000 00000011272 13726205233 0030537 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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 = '2.1'
COPYRIGHT = u'Copyright(C) 2011-2014 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):
super(Galleroob, self).__init__(*args, **kwargs)
@defaultcount(10)
def do_search(self, pattern):
"""
search PATTERN
List galleries matching a PATTERN.
"""
if not pattern:
print('This command takes an argument: %s' % self.get_command_help('search', short=True), file=self.stderr)
return 2
self.start_format(pattern=pattern)
for gallery in self.do('search_galleries', 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 result in self.do('get_gallery', _id, backends=backend):
if result:
gallery = result
if not gallery:
print('Gallery not found: %s' % _id, file=self.stderr)
return 3
self.weboob[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 self.weboob[backend].iter_gallery_images(gallery):
i += 1
if i < first:
continue
self.weboob[backend].fillobj(img, ('url', 'data'))
if img.data is None:
self.weboob[backend].fillobj(img, ('url', 'data'))
if img.data is None:
print("Couldn't get page %d, exiting" % i, file=self.stderr)
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, 'wb') 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('Gallery not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(gallery)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/geolooc/ 0000775 0000000 0000000 00000000000 13726205233 0025675 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/geolooc/__init__.py 0000664 0000000 0000000 00000001424 13726205233 0030007 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .geolooc import Geolooc
__all__ = ['Geolooc']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/geolooc/geolooc.py 0000664 0000000 0000000 00000003130 13726205233 0027673 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.capabilities.geolocip import CapGeolocIp
from weboob.tools.application.repl import ReplApplication
__all__ = ['Geolooc']
class Geolooc(ReplApplication):
APPNAME = 'geolooc'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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('Syntax: %s ipaddr' % argv[0], file=self.stderr)
return 2
for location in self.do('get_location', argv[1]):
if location.lt and location.lg:
location.osmlink = u'http://www.openstreetmap.org/?mlat=%s&mlon=%s#map=13/%s/%s' % (location.lt, location.lg, location.lt, location.lg)
self.format(location)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/handjoob/ 0000775 0000000 0000000 00000000000 13726205233 0026032 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/handjoob/__init__.py 0000664 0000000 0000000 00000001415 13726205233 0030144 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .handjoob import Handjoob
__all__ = ['Handjoob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/handjoob/handjoob.py 0000664 0000000 0000000 00000011546 13726205233 0030177 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2012-YEAR 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 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 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('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
job_advert = self.get_object(_id, 'get_job_advert')
if not job_advert:
print('Job advert not found: %s' % _id, file=self.stderr)
return 3
self.start_format()
self.format(job_advert)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/havedate/ 0000775 0000000 0000000 00000000000 13726205233 0026027 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/havedate/__init__.py 0000664 0000000 0000000 00000001427 13726205233 0030144 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .havedate import HaveDate
__all__ = ['HaveDate']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/havedate/havedate.py 0000664 0000000 0000000 00000024162 13726205233 0030167 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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.items():
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 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('Error: missing parameters.', file=self.stderr)
return 2
for optim_name in optims_names.split():
backends_optims = {}
for optim in self.do('get_optimization', optim_name, backends=backend_names):
if optim:
backends_optims[optim.backend] = optim
for backend_name, optim in backends_optims.items():
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.items():
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('Error: missing parameters.', file=self.stderr)
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 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(optim.backend, optim_name)
ret = getattr(optim, function)()
self.stdout.write(' ' + optim.backend)
if not ret:
self.stdout.write('(failed)')
self.stdout.flush()
if store:
if function == 'start' and ret:
storage_optim.add(optim.backend)
elif function == 'stop':
try:
storage_optim.remove(optim.backend)
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 optim in self.do('iter_optimizations', backends=backend):
optims.add(optim.id)
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 backend_name not in [b.name for b in self.enabled_backends]:
print('Error: No such backend "%s"' % backend_name, file=self.stderr)
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 optim in self.do('iter_optimizations', backends=backend_name):
if optims_names is not None and optim.id not in optims_names:
continue
if optim.is_running():
status = 'RUNNING'
else:
status = '-------'
if optim.id not in optims:
optims[optim.id] = {optim.backend: status}
else:
optims[optim.id][optim.backend] = status
backends.add(optim.backend)
backends = sorted(backends)
for name, backends_status in optims.items():
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("No such command '%s'" % cmd, file=self.stderr)
return 1
def do_events(self, line):
"""
events
Display dating events.
"""
self.change_path([u'events'])
self.start_format()
for event in self.do('iter_events'):
self.cached_format(event)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/monboob/ 0000775 0000000 0000000 00000000000 13726205233 0025701 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/monboob/__init__.py 0000664 0000000 0000000 00000001424 13726205233 0030013 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .monboob import Monboob
__all__ = ['Monboob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/monboob/monboob.py 0000664 0000000 0000000 00000032625 13726205233 0027716 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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.compat import unicode
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):
super(MonboobScheduler, self).__init__()
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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('Configuration error: interval must be an integer >0.', file=self.stderr)
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('Configuration error: html must be 0 or 1.', file=self.stderr)
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('Unable to send an empty message', file=self.stderr)
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:
backend_name, thread_id = to.split('.', 1)
else:
backend_name = to
thread_id = None
else:
# This is a reply
try:
backend_name, id = reply_to.split('.', 1)
thread_id, parent_id = id.rsplit('.', 1)
except ValueError:
print('In-Reply-To header might be in form ', file=self.stderr)
return 1
# Default use the To header field to know the backend to use.
if to and backend_name != to:
backend_name = to
try:
backend = self.weboob.backend_instances[backend_name]
except KeyError:
print('Backend %s not found' % backend_name, file=self.stderr)
return 1
if not backend.has_caps(CapMessagesPost):
print('The backend %s does not implement CapMessagesPost' % backend_name, file=self.stderr)
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.name, 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 message in self.weboob.do('iter_unread_messages'):
if self.send_email(message.backend, message):
self.weboob[message.backend].set_message_read(message)
except CallErrors as e:
self.bcall_errors_handler(e)
def send_email(self, backend_name, mail):
domain = self.config.get('domain')
recipient = self.config.get('recipient')
parent_message = mail.parent
references = []
while parent_message:
references.append(u'<%s.%s@%s>' % (backend_name, mail.parent.full_id, domain))
parent_message = parent_message.parent
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 references:
msg['In-Reply-To'] = references[0]
msg['References'] = u" ".join(reversed(references))
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-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/parceloob/ 0000775 0000000 0000000 00000000000 13726205233 0026214 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/parceloob/__init__.py 0000664 0000000 0000000 00000001425 13726205233 0030327 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .parceloob import Parceloob
__all__ = ['Parceloob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/parceloob/parceloob.py 0000664 0000000 0000000 00000015662 13726205233 0030546 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2013-YEAR 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('Error: the parcel "%s" is not found' % line, file=self.stderr)
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('Error: the parcel "%s" is not found. Did you provide the full id@backend parameter?' % line, file=self.stderr)
return 2
try:
parcels.remove(parcel.fullid)
except KeyError:
print("Error: parcel \"%s\" wasn't tracked" % parcel.fullid, file=self.stderr)
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 = list(map(get_backend_name, self.enabled_backends))
self.start_format()
# XXX cleaning of cached objects may be by start_format()?
self.objects = []
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('Error: parcel not found', file=self.stderr)
return 2
self.start_format()
self.format(parcel)
for event in parcel.history:
self.format(event)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/pastoob/ 0000775 0000000 0000000 00000000000 13726205233 0025715 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/pastoob/__init__.py 0000664 0000000 0000000 00000001423 13726205233 0030026 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .pastoob import Pastoob
__all__ = ['Pastoob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/pastoob/pastoob.py 0000664 0000000 0000000 00000017452 13726205233 0027747 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from base64 import b64decode, b64encode
import os
import codecs
import re
from random import choice
import sys
from weboob.capabilities.paste import CapPaste, PasteNotFound
from weboob.tools.application.repl import ReplApplication
__all__ = ['Pastoob']
class Pastoob(ReplApplication):
APPNAME = 'pastoob'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2011-YEAR 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('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
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('Paste not found: %s' % _id, file=self.stderr)
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('This command takes an argument: %s' % self.get_command_help(command, short=True), file=self.stderr)
return 2
try:
paste = self.get_object(_id, 'get_paste', ['contents'])
except PasteNotFound:
print('Paste not found: %s' % _id, file=self.stderr)
return 3
if not paste:
print('Unable to handle paste: %s' % _id, file=self.stderr)
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('Aborting.', file=self.stderr)
return 1
if sys.version_info.major >= 3:
output = self.stdout.buffer
else:
output = self.stdout.stream
output.write(b64decode(paste.contents))
else:
if sys.version_info.major < 3:
output = codecs.getwriter(self.encoding)(self.stdout)
else:
output = self.stdout
output.write(paste.contents)
# add a newline unless we are writing
# in a file or in a pipe
if output.isatty():
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:
if sys.version_info.major >= 3:
contents = self.stdin.buffer.read()
else:
contents = self.stdin.read()
else:
contents = self.acquire_input()
if not len(contents):
print('Empty paste, aborting.', file=self.stderr)
return 1
else:
try:
if binary:
m = open(filename, 'rb')
else:
m = codecs.open(filename, encoding=self.options.encoding or self.encoding)
with m as fp:
contents = fp.read()
except IOError as e:
print('Unable to open file "%s": %s' % (filename, e.strerror), file=self.stderr)
return 1
if binary:
contents = b64encode(contents).decode('ascii')
# 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(list(backends.keys()))])
else:
print('No suitable backend found.', file=self.stderr)
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.items():
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-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/radioob/ 0000775 0000000 0000000 00000000000 13726205233 0025665 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/radioob/__init__.py 0000664 0000000 0000000 00000001424 13726205233 0027777 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .radioob import Radioob
__all__ = ['Radioob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/radioob/radioob.py 0000664 0000000 0000000 00000037343 13726205233 0027670 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import re
from shutil import which
import subprocess
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon\nCopyright(C) YEAR 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):
super(Radioob, self).__init__(*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 [DIRECTORY]
Download an audio file
"""
_id, dest = self.parse_command_args(line, 2, 1)
obj = self.retrieve_obj(_id)
if obj is None:
print('No object matches with this id:', _id, file=self.stderr)
return 3
if isinstance(obj, BaseAudio):
streams = [obj]
else:
streams = obj.tracks_list
if len(streams) == 0:
print('Radio or Audio file not found:', _id, file=self.stderr)
return 3
for stream in streams:
self.download_file(stream, dest)
def download_file(self, audio, dest):
_obj = self.get_object(audio.id, 'get_audio', ['url', 'title'])
if not _obj:
print('Audio file not found: %s' % audio.id, file=self.stderr)
return 3
if not _obj.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
audio.url = _obj.url
def check_exec(executable):
if which(executable) is None:
print('Please install "%s"' % executable, file=self.stderr)
return False
return True
def audio_to_file(_audio):
ext = _audio.ext
if not ext:
ext = 'audiofile'
title = _audio.title if _audio.title else _audio.id
return '%s.%s' % (re.sub('[?:/]', '-', title), 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
subprocess.call(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('This command takes an argument: %s' % self.get_command_help('play', short=True), file=self.stderr)
return 2
try:
stream_id = int(stream_id)
except (ValueError, TypeError):
stream_id = 0
obj = self.retrieve_obj(_id)
if obj is None:
print('No object matches with this id:', _id, file=self.stderr)
return 3
if isinstance(obj, Radio):
try:
streams = [obj.streams[stream_id]]
except IndexError:
print('Stream %d not found' % stream_id, file=self.stderr)
return 1
elif isinstance(obj, BaseAudio):
streams = [obj]
else:
streams = obj.tracks_list
if len(streams) == 0:
print('Radio or Audio file not found:', _id, file=self.stderr)
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 = next(r.iter_content(512)).decode('utf-8', 'replace')
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('This command takes an argument: %s' % self.get_command_help('playlist'), file=self.stderr)
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('Audio file not found: %s' % _id, file=self.stderr)
return 3
if not audio.url:
print('Error: the direct URL is not available.', file=self.stderr)
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('Audio file not found: %s' % _id, file=self.stderr)
return 3
if not audio_to_remove.url:
print('Error: the direct URL is not available.', file=self.stderr)
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('Playlist command only support "add", "remove", "display" and "export" arguments.', file=self.stderr)
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('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
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('No object matches with this id:', _id, file=self.stderr)
return 3
self.format(obj)
@defaultcount(10)
def do_search(self, pattern=None):
"""
search (radio|song|file|album|playlist) PATTERN
List (radio|song|file|album|playlist) matching a PATTERN.
If PATTERN is not given, this command will list all the (radio|song|album|playlist).
"""
if not pattern:
print('This command takes an argument: %s' % self.get_command_help('search'), file=self.stderr)
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":
if 'search' in self.commands_formatters:
self.set_formatter('radio_list')
for radio in self.do('iter_radios_search', pattern=args):
self.add_object(radio)
self.format(radio)
elif cmd == "song" or cmd == "file":
if 'search' in self.commands_formatters:
self.set_formatter('song_list')
for audio in self.do('search_audio', pattern=args):
self.add_object(audio)
self.format(audio)
elif cmd == "album":
if 'search' in self.commands_formatters:
self.set_formatter('song_list')
for album in self.do('search_album', pattern=args):
self.add_object(album)
self.format(album)
elif cmd == "playlist":
if 'search' in self.commands_formatters:
self.set_formatter('song_list')
for playlist in self.do('search_playlist', pattern=args):
self.add_object(playlist)
self.format(playlist)
else:
print('Search command only supports "radio", "song", "file", "album" and "playlist" arguments.', file=self.stderr)
return 2
def do_ls(self, line):
"""
ls
List radios
"""
ret = super(Radioob, self).do_ls(line)
return ret
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/shopoob/ 0000775 0000000 0000000 00000000000 13726205233 0025717 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/shopoob/__init__.py 0000664 0000000 0000000 00000001424 13726205233 0030031 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .shopoob import Shopoob
__all__ = ['Shopoob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/shopoob/shopoob.py 0000664 0000000 0000000 00000015044 13726205233 0027746 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2015 Christophe Lampin
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from decimal import Decimal
from weboob.capabilities.base import empty
from weboob.capabilities.shop import CapShop, Order, Item
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['Shopoob']
class OrdersFormatter(IFormatter):
MANDATORY_FIELDS = ('id', 'date', 'total')
def start_format(self, **kwargs):
self.output(' Id Date Total ')
self.output('-----------------------------+------------+-----------')
def format_obj(self, obj, alias):
date = obj.date.strftime('%Y-%m-%d') if not empty(obj.date) else ''
total = obj.total or Decimal('0')
result = u'%s %s %s' % (self.colored('%-28s' % obj.fullid, 'yellow'),
self.colored('%-10s' % date, 'blue'),
self.colored('%9.2f' % total, 'green'))
return result
def flush(self):
self.output(u'----------------------------+------------+-----------')
class ItemsFormatter(IFormatter):
MANDATORY_FIELDS = ('label', 'url', 'price')
def start_format(self, **kwargs):
self.output(' Label Url Price ')
self.output('---------------------------------------------------------------------------+---------------------------------------------+----------')
def format_obj(self, obj, alias):
price = obj.price or Decimal('0')
result = u'%s %s %s' % (self.colored('%-75s' % obj.label[:75], 'yellow'),
self.colored('%-43s' % obj.url, 'magenta'),
self.colored('%9.2f' % price, 'green'))
return result
def flush(self):
self.output(u'---------------------------------------------------------------------------+---------------------------------------------+----------')
class PaymentsFormatter(IFormatter):
MANDATORY_FIELDS = ('date', 'method', 'amount')
def start_format(self, **kwargs):
self.output(' Date Method Amount ')
self.output('-----------+-----------------+----------')
def format_obj(self, obj, alias):
date = obj.date.strftime('%Y-%m-%d') if not empty(obj.date) else ''
amount = obj.amount or Decimal('0')
result = u'%s %s %s' % (self.colored('%-10s' % date, 'blue'),
self.colored('%-17s' % obj.method, 'yellow'),
self.colored('%9.2f' % amount, 'green'))
return result
def flush(self):
self.output(u'-----------+-----------------+----------')
class Shopoob(ReplApplication):
APPNAME = 'shopoob'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2015 Christophe Lampin'
DESCRIPTION = 'Console application to obtain details and status of e-commerce orders.'
SHORT_DESCRIPTION = "Obtain details and status of e-commerce orders"
CAPS = CapShop
COLLECTION_OBJECTS = (Order, )
EXTRA_FORMATTERS = {'orders': OrdersFormatter,
'items': ItemsFormatter,
'payments': PaymentsFormatter
}
DEFAULT_FORMATTER = 'table'
COMMANDS_FORMATTERS = {'orders': 'orders',
'items': 'items',
'payments': 'payments',
'ls': 'orders',
}
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
@defaultcount(10)
def do_orders(self, line):
"""
orders [BACKEND_NAME]
Get orders of a backend.
If no BACKEND_NAME given, display all orders of all backends.
"""
if len(line) > 0:
backend_name = line
else:
backend_name = None
self.do_count(str(self.options.count)) # Avoid raise of MoreResultsAvailable
l = []
for order in self.do('iter_orders', backends=backend_name):
l.append(order)
self.start_format()
for order in sorted(l, self.comp_object):
self.format(order)
# Order by date DESC
def comp_object(self, obj1, obj2):
if obj1.date == obj2.date:
return 0
elif obj1.date < obj2.date:
return 1
else:
return -1
def do_items(self, id):
"""
items [ID]
Get items of orders.
"""
l = []
id, backend_name = self.parse_id(id, unique_backend=True)
if not id:
print('Error: please give a order ID (hint: use orders command)', file=self.stderr)
return 2
else:
l.append((id, backend_name))
for id, backend in l:
names = (backend,) if backend is not None else None
# TODO: Use specific formatter
mysum = Item()
mysum.label = u"Sum"
mysum.url = u"Generated by shopoob"
mysum.price = Decimal("0.")
self.start_format()
for item in self.do('iter_items', id, backends=names):
self.format(item)
mysum.price = item.price + mysum.price
self.format(mysum)
def do_payments(self, id):
"""
payments [ID]
Get payments of orders.
If no ID given, display payment of all backends.
"""
id, backend_name = self.parse_id(id, unique_backend=True)
if not id:
print('Error: please give a order ID (hint: use orders command)', file=self.stderr)
return 2
self.start_format()
for payment in self.do('iter_payments', id, backends=backend_name):
self.format(payment) woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/suboob/ 0000775 0000000 0000000 00000000000 13726205233 0025537 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/suboob/__init__.py 0000664 0000000 0000000 00000001415 13726205233 0027651 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .suboob import Suboob
__all__ = ['Suboob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/suboob/suboob.py 0000664 0000000 0000000 00000017304 13726205233 0027407 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
import sys
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2013-YEAR 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('Subtitle not found: %s' % id, file=self.stderr)
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('Subtitle not found: %s' % id, file=self.stderr)
return 3
if dest is None:
ext = subtitle.ext
if empty(ext):
ext = 'zip'
dest = '%s.%s' % (subtitle.name, ext)
for buf in self.do('get_subtitle_file', subtitle.id, backends=subtitle.backend):
if buf:
if dest == '-':
if sys.version_info.major >= 3:
self.stdout.buffer.write(buf)
else:
self.stdout.stream.write(buf)
else:
try:
with open(dest, 'wb') as f:
f.write(buf)
except IOError as e:
print('Unable to write file in "%s": %s' % (dest, e), file=self.stderr)
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 subtitle in self.do('iter_subtitles', language=language, pattern=pattern):
self.cached_format(subtitle)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/translaboob/ 0000775 0000000 0000000 00000000000 13726205233 0026554 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/translaboob/__init__.py 0000664 0000000 0000000 00000001434 13726205233 0030667 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .translaboob import Translaboob
__all__ = ['Translaboob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/translaboob/translaboob.py 0000664 0000000 0000000 00000011745 13726205233 0031444 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
import re
from weboob.capabilities.translate import CapTranslate, TranslationFail, LanguageNotSupported
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.formatters.iformatter import IFormatter
from babel.core import Locale, UnknownLocaleError
from babel.localedata import locale_identifiers
__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 = '2.1'
COPYRIGHT = 'Copyright(C) 2012-YEAR 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',
}
def parse_lang(self, s):
try:
locale = Locale.parse(s)
except UnknownLocaleError:
pattern = re.compile(r'\b%s\b' % re.escape(s), re.I)
for locale_id in locale_identifiers():
locale = Locale.parse(locale_id)
if pattern.search(locale.english_name):
return locale.language
return s
else:
return locale.language
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:
lan_from = self.parse_lang(lan_from)
lan_to = self.parse_lang(lan_to)
if not text or text == '-':
text = self.acquire_input()
self.start_format(source=text)
for translation in self.do('translate', lan_from, lan_to, text):
self.format(translation)
except (TranslationFail, LanguageNotSupported) as error:
print(error, file=self.stderr)
pass
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/traveloob/ 0000775 0000000 0000000 00000000000 13726205233 0026243 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/traveloob/__init__.py 0000664 0000000 0000000 00000001432 13726205233 0030354 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .traveloob import Traveloob
__all__ = ['Traveloob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/traveloob/traveloob.py 0000664 0000000 0000000 00000015226 13726205233 0030620 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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') if obj.time else '??:??', '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') if obj.time else '??:??', '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 = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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 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('Departure and arrival aren\'t on the same backend', file=self.stderr)
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('Invalid datetime value: %s' % e, file=self.stderr)
print('Please enter a datetime in form "yyyy-mm-dd HH:MM" or "HH:MM".', file=self.stderr)
return 1
for 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('Invalid datetime value: %s' % e, file=self.stderr)
print('Please enter a datetime in form "yyyy-mm-dd HH:MM" or "HH:MM".', file=self.stderr)
return 1
for 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-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/videoob/ 0000775 0000000 0000000 00000000000 13726205233 0025675 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/videoob/__init__.py 0000664 0000000 0000000 00000001426 13726205233 0030011 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .videoob import Videoob
__all__ = ['Videoob']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/videoob/image2xterm.py 0000664 0000000 0000000 00000011607 13726205233 0030500 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright(C) 2017 Vincent A
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
#
# This file is distributed under the WTFPLv2 license.
from __future__ import division
import sys
import os
import PIL.Image as Image
import PIL.ImageColor as PILColor
from weboob.tools.compat import range
"""
XTerm can decode sequences and display 256 colors:
- 16 system colors, supported by many terms [0-15]
- 216 colors (not the web-safe palette) [16-231]
- 24 grey colors (excluding black and white) [231-255]
Other terminals do support these 256 colors escape codes, e.g. roxterm, xfce-terminal.
"""
__all__ = ('make256xterm', 'imageRGB_to_256', 'image2xterm', 'image256_to_ansi')
def make256xterm():
"""Return a [r, g, b, r, g, b, ...] table with the colors of the 256 xterm colors.
The table is indexed: [0:3] corresponds to color 0.
"""
color256 = []
# standard 16 colors
color256 += list(sum((PILColor.getrgb(c) for c in
'''black maroon green olive navy purple teal silver
gray red lime yellow blue fuchsia aqua white'''.split()), ()))
steps = (0, 95, 135, 175, 215, 255)
for r in steps:
for g in steps:
for b in steps:
color256 += [r, g, b]
# grays
for v in range(8, 248, 10):
color256 += [v, v, v]
assert len(color256) == 256 * 3
return color256
TABLE_XTERM_256 = make256xterm()
def imageRGB_to_256(im):
"""Returns `im` converted to the XTerm 256 colors palette.
The image should be resized *before* applying this function.
"""
paletter = Image.new('P', (1, 1))
paletter.putpalette(TABLE_XTERM_256)
return im.quantize(palette=paletter)
def image256_to_ansi(im, halfblocks):
"""Print PIL image `im` to `fd` using XTerm escape codes.
1 pixel corresponds to exactly 1 character on the terminal. 1 row of pixels is terminated by a newline.
`im` has to be a 256 colors image (ideally in the XTerm palette if you want it to make sense).
`im` should be resized appropriately to fit terminal size and character aspect (characters are never square).
"""
buf = []
pix = im.load()
w, h = im.size
if halfblocks:
yrange = range(0, h, 2)
else:
yrange = range(h)
for y in yrange:
for x in range(w):
if halfblocks:
if y + 1 >= h:
buf.append(u'\x1b[38;5;%dm\u2580' % pix[x, y])
else:
buf.append(u'\x1b[38;5;%dm\x1b[48;5;%dm\u2580' % (pix[x, y], pix[x, y + 1]))
else:
buf.append(u'\x1b[48;5;%dm ' % pix[x, y])
buf.append(u'\x1b[0m%s' % os.linesep)
return ''.join(buf)
def image2xterm(imagepath, newsize=(80, 24), halfblocks=True):
image = Image.open(imagepath)
image.load()
stretch = 2
curratio = image.size[1] / (image.size[0] * stretch)
targetsize = newsize[0], int(newsize[0] * curratio)
if targetsize[1] > newsize[1]:
targetsize = int(newsize[1] / curratio), newsize[1]
if halfblocks:
targetsize = targetsize[0], targetsize[1] * 2
image = image.convert('RGB')
image = image.resize(targetsize, Image.ANTIALIAS)
image2 = imageRGB_to_256(image)
return image256_to_ansi(image2, halfblocks=halfblocks)
def _getoutsize(infoname, default):
"""Get suitable dimension for tty or default"""
if sys.stdout.isatty():
import curses
curses.setupterm()
return curses.tigetnum(infoname)
else:
return default
def get_term_size():
return (_getoutsize('cols', 80), _getoutsize('lines', 24))
def main():
import argparse
parser = argparse.ArgumentParser(description='Print an image on terminal with 256 colors palette')
parser.add_argument('-r', '--rows', dest='rows', metavar='SIZE', type=int)
parser.add_argument('-c', '--columns', dest='cols', metavar='SIZE', type=int)
parser.add_argument('--spaces', action='store_true')
parser.add_argument('file')
opts = parser.parse_args()
if opts.rows is None:
opts.rows = _getoutsize('lines', 24)
if opts.cols is None:
opts.cols = _getoutsize('cols', 80)
data = image2xterm(opts.file, (opts.cols, opts.rows), halfblocks=not opts.spaces)
sys.stdout.write(data)
if __name__ == '__main__':
main()
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/videoob/videoob.py 0000664 0000000 0000000 00000027647 13726205233 0027716 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from io import BytesIO
import os
from shutil import which
import subprocess
import requests
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
from weboob.tools.compat import urlparse
from .image2xterm import image2xterm, get_term_size
__all__ = ['Videoob']
class VideoListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'duration', 'date')
DISPLAYED_FIELDS = ('author', 'rating', 'thumbnail', 'rating_max')
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)
if hasattr(obj, 'thumbnail') and not empty(obj.thumbnail) and not empty(obj.thumbnail.data):
result += u'\n'
result += image2xterm(BytesIO(obj.thumbnail.data), newsize=get_term_size())
return result
class Videoob(ReplApplication):
APPNAME = 'videoob'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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):
super(Videoob, self).__init__(*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('Error: the direct URL is not available.', file=self.stderr)
return 4
def check_exec(executable):
if which(executable) is None:
print('Please install "%s"' % executable, file=self.stderr)
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()
parsed_uri = urlparse(video.url)
baseurl = '{uri.scheme}://{uri.netloc}'.format(uri=parsed_uri)
for line in self.read_url(video.url):
line = line.decode('utf-8')
if not line.startswith('#'):
if not line.startswith('http'):
line = u'%s%s' % (baseurl, line)
content += (line,)
args = ('wget', '-nv',) + 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
self.logger.debug(' '.join(args))
subprocess.call(args)
def read_url(self, url):
r = requests.get(url, stream=True)
return r.iter_lines()
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('Video not found: %s' % _id, file=self.stderr)
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('This command takes an argument: %s' % self.get_command_help('play', short=True), file=self.stderr)
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('Video not found: %s' % _id, file=self.stderr)
return 3
if not video.url:
print('Error: the direct URL is not available.', file=self.stderr)
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('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
self.start_format()
for _id in line.split(' '):
video = self.get_object(_id, 'get_video')
if not video:
print('Video not found: %s' % _id, file=self.stderr)
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('This command can be used only in interactive mode.', file=self.stderr)
return 1
if not line:
print('This command takes an argument: %s' % self.get_command_help('playlist'), file=self.stderr)
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('Video not found: %s' % _id, file=self.stderr)
return 3
if not video.url:
print('Error: the direct URL is not available.', file=self.stderr)
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('Video not found: %s' % _id, file=self.stderr)
return 3
if not video_to_remove.url:
print('Error: the direct URL is not available.', file=self.stderr)
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('Playlist command only support "add", "remove", "display", "download" and "export" arguments.', file=self.stderr)
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('This command takes an argument: %s' % self.get_command_help('search', short=True), file=self.stderr)
return 2
self.change_path([u'search'])
self.start_format(pattern=pattern)
for video in self.do('search_videos', pattern=pattern, nsfw=self.nsfw):
self.cached_format(video)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/webcontentedit/ 0000775 0000000 0000000 00000000000 13726205233 0027264 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/webcontentedit/__init__.py 0000664 0000000 0000000 00000001451 13726205233 0031376 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .webcontentedit import WebContentEdit
__all__ = ['WebContentEdit']
webcontentedit.py 0000664 0000000 0000000 00000017111 13726205233 0032576 0 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import tempfile
import shlex
import subprocess
from shutil import which
from weboob.core.bcall import CallErrors
from weboob.capabilities.content import CapContent, Revision, Content
from weboob.tools.application.repl import ReplApplication, defaultcount
__all__ = ['WebContentEdit']
class WebContentEdit(ReplApplication):
APPNAME = 'webcontentedit'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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 content in self.do('get_content', _id, backends=backend_names) if content]
if len(contents) == 0:
print('No contents found', file=self.stderr)
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('w+t', prefix='%s_' % content.id.replace(os.path.sep, '_'), dir=tmpdir, delete=False) as f:
if content.content is None:
content.content = ''
f.write(content.content)
paths[f.name] = 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(which(editor) or '/')).replace('.nox', '')):
params = ['-p']
subprocess.call([editor, *params, *paths])
for path, content in paths.items():
with open(path, 'r') as f:
try:
data = f.read()
except UnicodeError:
pass
if content.content != data:
content.content = data
else:
contents.remove(content)
if len(contents) == 0:
print('No changes. Abort.', file=self.stderr)
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.items() if c == content][0]
self.stdout.write('Pushing %s...' % content.id)
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("Multiple ids not supported with pipe", file=self.stderr)
return 2
message, minor = '', False
data = self.stdin.read()
contents[0].content = data
errors = CallErrors([])
for content in contents:
self.stdout.write('Pushing %s...' % content.id)
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
def do_create(self, line):
"""
create TITLE BACKEND
"""
args = shlex.split(line)
title, backend = args
if self.stdin.isatty():
editor = os.environ.get('EDITOR', 'vi')
with tempfile.NamedTemporaryFile('w+t', suffix='.md') as fd:
subprocess.call([editor, fd.name])
data = fd.read()
else:
data = self.stdin.read()
content = Content()
content.title = args[0]
content.content = data
content.backend = backend
content = next(iter(self.do('push_content', content, message='', minor=False, backends=[content.backend]))) or content
if content.url:
print('Pushed to', content.url, file=self.stdout)
@defaultcount(10)
def do_log(self, line):
"""
log ID
Display log of a page
"""
if not line:
print('Error: please give a page ID', file=self.stderr)
return 2
_id, backend_name = self.parse_id(line)
backend_names = (backend_name,) if backend_name is not None else self.enabled_backends
self.start_format()
for 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('Error: please give a page ID', file=self.stderr)
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('Error: please give a page ID', file=self.stderr)
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
output = self.stdout
for contents in [content for 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 output.isatty():
output.write('\n')
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobcfg/ 0000775 0000000 0000000 00000000000 13726205233 0026203 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobcfg/__init__.py 0000664 0000000 0000000 00000001432 13726205233 0030314 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .weboobcfg import WeboobCfg
__all__ = ['WeboobCfg']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobcfg/weboobcfg.py 0000664 0000000 0000000 00000025534 13726205233 0030523 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
from collections import OrderedDict
from weboob.capabilities.account import CapAccount
from weboob.exceptions import ModuleLoadError
from weboob.tools.application.repl import ReplApplication
from weboob.tools.application.console import ConsoleProgress
from weboob.tools.application.formatters.iformatter import IFormatter
__all__ = ['WeboobCfg']
class ModuleInfoFormatter(IFormatter):
def format_dict(self, minfo):
result = '.------------------------------------------------------------------------------.\n'
result += '| Module %-69s |\n' % minfo['name']
result += "+-----------------.------------------------------------------------------------'\n"
result += '| Version | %s\n' % minfo['version']
result += '| Maintainer | %s\n' % minfo['maintainer']
result += '| License | %s\n' % minfo['license']
result += '| Description | %s\n' % minfo['description']
result += '| Capabilities | %s\n' % ', '.join(minfo['capabilities'])
result += '| Installed | %s\n' % minfo['installed']
result += '| Location | %s\n' % minfo['location']
if 'config' in minfo:
first = True
for key, field in minfo['config'].items():
label = field['label']
if field['default'] is not None:
label += ' (default: %s)' % field['default']
if first:
result += '| | \n'
result += '| Configuration | %s: %s\n' % (key, label)
first = False
else:
result += '| | %s: %s\n' % (key, label)
result += "'-----------------'\n"
return result
class WeboobCfg(ReplApplication):
APPNAME = 'weboob-config'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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"
EXTRA_FORMATTERS = {'info_formatter': ModuleInfoFormatter}
COMMANDS_FORMATTERS = {'modules': 'table',
'list': 'table',
'info': 'info_formatter',
}
DISABLE_REPL = True
def load_default_backends(self):
pass
def do_add(self, line):
"""
add MODULE_NAME [BACKEND_NAME] [PARAMETERS ...]
Create a backend from a module. By default, if BACKEND_NAME is omitted,
that's the module name which is used.
You can specify parameters from command line in form "key=value".
"""
if not line:
print('You must specify a module name. Hint: use the "modules" command.', file=self.stderr)
return 2
module_name, options = self.parse_command_args(line, 2, 1)
if options:
options = options.split(' ')
else:
options = ()
backend_name = None
params = {}
# set backend params from command-line arguments
for option in options:
try:
key, value = option.split('=', 1)
except ValueError:
if backend_name is None:
backend_name = option
else:
print('Parameters have to be formatted "key=value"', file=self.stderr)
return 2
else:
params[key] = value
self.add_backend(module_name, backend_name or module_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('Error: backend "%s" not found.' % backend_name, file=self.stderr)
return 1
if not backend.has_caps(CapAccount):
print('Error: backend "%s" does not support accounts management' % backend_name, file=self.stderr)
return 1
mail = self.acquire_input()
if not backend.confirm_account(mail):
print('Error: Unable to confirm account creation', file=self.stderr)
return 1
return 0
def do_list(self, line):
"""
list [CAPS ..]
Show backends.
"""
caps = line.split()
for backend_name, module_name, params in sorted(self.weboob.backends_config.iter_backends()):
try:
module = self.weboob.modules_loader.get_or_load_module(module_name)
except ModuleLoadError as e:
self.logger.warning('Unable to load module %r: %s' % (module_name, e))
continue
if caps and not module.has_caps(*caps):
continue
row = OrderedDict([('Name', backend_name),
('Module', module_name),
('Configuration', ', '.join(
'%s=%s' % (key, ('*****' if key in module.config and module.config[key].masked
else value))
for key, value in params.items())),
])
self.format(row)
def do_enable(self, backend_name):
"""
enable NAME
Enable a backend.
"""
try:
self.weboob.backends_config.edit_backend(backend_name, {'_enabled': '1'})
except KeyError:
print('Backend instance "%s" does not exist' % backend_name, file=self.stderr)
return 1
def do_disable(self, backend_name):
"""
disable NAME
Disable a backend.
"""
try:
self.weboob.backends_config.edit_backend(backend_name, {'_enabled': '0'})
except KeyError:
print('Backend instance "%s" does not exist' % backend_name, file=self.stderr)
return 1
def do_remove(self, backend_name):
"""
remove NAME
Remove a backend.
"""
if not self.weboob.backends_config.remove_backend(backend_name):
print('Backend instance "%s" does not exist' % backend_name, file=self.stderr)
return 1
def do_edit(self, line):
"""
edit BACKEND
Edit a backend
"""
try:
self.edit_backend(line)
except KeyError:
print('Error: backend "%s" not found' % line, file=self.stderr)
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).items()):
row = OrderedDict([('Name', name),
('Capabilities', info.capabilities),
('Description', info.description),
('Installed', info.is_installed()),
])
self.format(row)
def do_info(self, line):
"""
info NAME
Display information about a module.
"""
if not line:
print('You must specify a module name. Hint: use the "modules" command.', file=self.stderr)
return 2
minfo = self.weboob.repositories.get_module_info(line)
if not minfo:
print('Module "%s" does not exist.' % line, file=self.stderr)
return 1
try:
module = self.weboob.modules_loader.get_or_load_module(line)
except ModuleLoadError:
module = None
self.start_format()
self.format(self.create_minfo_dict(minfo, module))
def create_minfo_dict(self, minfo, module):
module_info = {}
module_info['name'] = minfo.name
module_info['version'] = minfo.version
module_info['maintainer'] = minfo.maintainer
module_info['license'] = minfo.license
module_info['description'] = minfo.description
module_info['capabilities'] = minfo.capabilities
repo_ver = self.weboob.repositories.versions.get(minfo.name)
module_info['installed'] = '%s%s' % (
'yes' if module else 'no',
' (new version available)' if repo_ver and repo_ver > minfo.version else ''
)
module_info['location'] = '%s' % (minfo.url or os.path.join(minfo.path, minfo.name))
if module:
module_info['config'] = {}
for key, field in module.config.items():
module_info['config'][key] = {'label': field.label,
'default': field.default,
'description': field.description,
'regexp': field.regexp,
'choices': field.choices,
'masked': field.masked,
'required': field.required}
return module_info
def do_update(self, line):
"""
update
Update weboob.
"""
self.weboob.update(ConsoleProgress(self))
if self.weboob.repositories.errors:
print('Errors building modules: %s' % ', '.join(self.weboob.repositories.errors.keys()), file=self.stderr)
if not self.options.debug:
print('Use --debug to get more information.', file=self.stderr)
return 1
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobcli/ 0000775 0000000 0000000 00000000000 13726205233 0026213 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobcli/__init__.py 0000664 0000000 0000000 00000001432 13726205233 0030324 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .weboobcli import WeboobCli
__all__ = ['WeboobCli']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobcli/weboobcli.py 0000664 0000000 0000000 00000003412 13726205233 0030532 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from weboob.tools.application.repl import ReplApplication
__all__ = ['WeboobCli']
class WeboobCli(ReplApplication):
APPNAME = 'weboob-cli'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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("Syntax: %s capability method [args ..]" % argv[0], file=self.stderr)
return 2
cap_s = argv[1]
cmd = argv[2]
args = argv[3:]
self.load_backends(cap_s)
for obj in self.do(cmd, *args):
self.format(obj)
self.flush()
return 0
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobdebug/ 0000775 0000000 0000000 00000000000 13726205233 0026532 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobdebug/__init__.py 0000664 0000000 0000000 00000001441 13726205233 0030643 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .weboobdebug import WeboobDebug
__all__ = ['WeboobDebug']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobdebug/weboobdebug.py 0000664 0000000 0000000 00000007107 13726205233 0031375 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
from optparse import OptionGroup
from weboob.tools.application.base import Application
from weboob.browser.elements import generate_table_element
class WeboobDebug(Application):
APPNAME = 'weboob-debug'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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('Usage: %s BACKEND' % argv[0], file=self.stderr)
return 1
try:
backend = self.weboob.load_backends(names=[backend_name])[backend_name]
except KeyError:
print(u'Unable to load backend "%s"' % backend_name, file=self.stderr)
return 1
locs = dict(backend=backend, browser=backend.browser,
application=self, weboob=self.weboob,
generate_table_element=generate_table_element)
banner = 'Weboob debug shell\nBackend "%s" loaded.\nAvailable variables:\n' % backend_name \
+ '\n'.join([' %s: %s' % (k, v) for k, v in locs.items()])
if self.options.bpython:
funcs = [self.bpython, self.ipython, self.python]
else:
funcs = [self.ipython, self.bpython, self.python]
self.launch(funcs, locs, banner)
def launch(self, funcs, locs, banner):
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-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobmain/ 0000775 0000000 0000000 00000000000 13726205233 0026370 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobmain/__init__.py 0000664 0000000 0000000 00000001435 13726205233 0030504 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .weboobmain import WeboobMain
__all__ = ['WeboobMain']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobmain/weboobmain.py 0000775 0000000 0000000 00000015205 13726205233 0031072 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim: ft=python et softtabstop=4 cinoptions=4 shiftwidth=4 ts=4 ai
# Copyright(C) 2009-2017 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import absolute_import
from __future__ import print_function
import inspect
import os
import re
import sys
from collections import OrderedDict
from datetime import datetime, timedelta
import weboob.applications
from weboob.tools.application.console import ConsoleApplication
try:
from weboob.tools.application.qt5 import QtApplication
except ImportError:
class QtApplication(object):
pass
__all__ = ['WeboobMain']
class WeboobMain(ConsoleApplication):
APPNAME = 'weboob'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR The Weboob Team'
DESCRIPTION = "This is a console script to launch weboob applications,"
SHORT_DESCRIPTION = "launch weboob applications"
UPDATE_DAYS_DELAY = 20
def main(self):
interactive = sys.stdout.isatty()
if interactive:
self.update()
capApplicationDict = self.init_CapApplicationDict()
if len(sys.argv) >= 2:
try:
cmd = getattr(self, 'cmd_%s' % sys.argv[1])
except AttributeError:
pass
else:
cmd()
return
cap = sys.argv.pop(1)
if cap not in capApplicationDict:
if interactive:
print('Unknown capability, please choose one in the following list')
cap = self.choose_capability(capApplicationDict)
else:
cap = None
else:
if interactive:
cap = self.choose_capability(capApplicationDict)
else:
cap = None
def appsortkey(app):
if issubclass(app, QtApplication):
return '1' + app.APPNAME
else:
return '0' + app.APPNAME
if cap:
applications = capApplicationDict[cap]
applications = sorted(set(applications), key=appsortkey)
application = applications[0] if len(applications) == 1 else self.choose_application(applications)
application.run()
else:
print('Please provide a capability.')
def cmd_update(self):
self.weboob.update()
def update(self):
for repository in self.weboob.repositories.repositories:
update_date = datetime.strptime(str(repository.update), '%Y%m%d%H%M')
if (datetime.now() - timedelta(days=self.UPDATE_DAYS_DELAY)) > update_date:
update = self.ask('The repositories have not been updated for %s days, do you want to update them ? (y/n)'
% self.UPDATE_DAYS_DELAY,
default='n')
if update.upper() == 'Y':
self.weboob.repositories.update()
break
def choose_application(self, applications):
application = None
while not application:
for app in applications:
print(' %s%2d)%s %s: %s' % (self.BOLD,
applications.index(app) + 1,
self.NC,
app.APPNAME,
app.DESCRIPTION))
r = self.ask(' Select an application', regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(applications):
continue
application = applications[r - 1]
return application
def choose_capability(self, capApplicationDict):
cap = None
caps = list(capApplicationDict.keys())
while cap not in caps:
for n, _cap in enumerate(caps):
print(' %s%2d)%s %s' % (self.BOLD, n + 1, self.NC, _cap))
r = self.ask(' Select a capability', regexp='(\d+|)', default='')
if not r.isdigit():
continue
r = int(r)
if r <= 0 or r > len(caps):
continue
cap = caps[r - 1]
return cap
def init_CapApplicationDict(self):
capApplicationDict = {}
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 not (m and '__init__.py' in files):
continue
application = self.get_applicaction_from_filename(m.group(1))
if not application:
continue
capabilities = self.get_application_capabilities(application)
if not capabilities:
continue
for capability in capabilities:
if capability in capApplicationDict:
capApplicationDict[capability].append(application)
else:
capApplicationDict[capability] = [application]
return OrderedDict([(k, v) for k, v in sorted(capApplicationDict.items())])
def get_application_capabilities(self, application):
if hasattr(application, 'CAPS') and application.CAPS:
_capabilities = list(application.CAPS) if isinstance(application.CAPS, tuple) else [application.CAPS]
return [os.path.splitext(os.path.basename(inspect.getfile(x)))[0] for x in _capabilities]
def get_applicaction_from_filename(self, name):
module = 'weboob.applications.%s.%s' % (name, name)
try:
_module = __import__(module, fromlist=['*'])
except ImportError:
return
_application = [x for x in dir(_module) if x.lower() == name]
if _application:
return getattr(_module, _application[0])
@classmethod
def run(cls):
try:
cls().main()
except KeyboardInterrupt:
print('')
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobrepos/ 0000775 0000000 0000000 00000000000 13726205233 0026574 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobrepos/__init__.py 0000664 0000000 0000000 00000001440 13726205233 0030704 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .weboobrepos import WeboobRepos
__all__ = ['WeboobRepos']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboobrepos/weboobrepos.py 0000664 0000000 0000000 00000021260 13726205233 0031475 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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
from weboob.tools.misc import find_exe
__all__ = ['WeboobRepos']
class WeboobRepos(ReplApplication):
APPNAME = 'weboob-repos'
VERSION = '2.1'
COPYRIGHT = 'Copyright(C) 2012-YEAR 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('Unable to open repository: %s' % e, file=self.stderr)
print('Use the "create" command before.', file=self.stderr)
return 1
r.build_index(source_path, index_file)
if r.signed:
sigfiles = [r.KEYRING, Repository.INDEX]
gpg = find_exe('gpg1') or find_exe('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.items():
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, filter=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:
gpg = find_exe('gpg2') or find_exe('gpg')
if not gpg:
raise Exception('Unable to find the gpg executable.')
# Find out which keys are allowed to sign
fingerprints = [gpgline.decode('utf-8').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(b'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')
def _archive_excludes(self, tarinfo):
filename = tarinfo.name
# Skip *.pyc files in tarballs.
if filename.endswith('.pyc'):
return
# Don't include *.png files in tarball
if filename.endswith('.png'):
return
return tarinfo
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboorrents/ 0000775 0000000 0000000 00000000000 13726205233 0026617 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboorrents/__init__.py 0000664 0000000 0000000 00000001440 13726205233 0030727 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .weboorrents import Weboorrents
__all__ = ['Weboorrents']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/weboorrents/weboorrents.py 0000664 0000000 0000000 00000015011 13726205233 0031540 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
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, NotLoaded, 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 and obj.size != NotLoaded:
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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', ('id', 'name', 'size', 'seeders', 'leechers', 'url', 'files', 'description'))
if not torrent:
print('Torrent not found: %s' % id, file=self.stderr)
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('Torrent not found: %s' % id, file=self.stderr)
return 3
dest = self.obj_to_filename(torrent, dest, '{id}-{name}.torrent')
try:
for 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('Unable to write .torrent in "%s": %s' % (dest, e), file=self.stderr)
return 1
return
except CallErrors as errors:
for backend, error, backtrace in errors:
if isinstance(error, MagnetOnly):
print(u'Error(%s): No direct URL available, '
u'please provide this magnet URL '
u'to your client:\n%s' % (backend, error.magnet), file=self.stderr)
return 4
else:
self.bcall_error_handler(backend, error, backtrace)
print('Torrent "%s" not found' % id, file=self.stderr)
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 torrent in self.do('iter_torrents', pattern=pattern):
self.cached_format(torrent)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/wetboobs/ 0000775 0000000 0000000 00000000000 13726205233 0026072 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/wetboobs/__init__.py 0000664 0000000 0000000 00000001427 13726205233 0030207 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .wetboobs import WetBoobs
__all__ = ['WetBoobs']
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/applications/wetboobs/wetboobs.py 0000664 0000000 0000000 00000011670 13726205233 0030275 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from weboob.capabilities.base import empty
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) if not empty(obj.low) else '?',
self.temperature_display(obj.high) if not empty(obj.high) else '?'
)
)
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 = '2.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR 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 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 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 forecast in self.do('iter_forecast', _id, backends=backend_name, caps=CapWeather):
self.format(forecast)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/ 0000775 0000000 0000000 00000000000 13726205233 0023243 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/__init__.py 0000664 0000000 0000000 00000002264 13726205233 0025360 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012-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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from .browsers import (
Browser, DomainBrowser, UrlNotAllowed, PagesBrowser,
LoginBrowser, need_login, AbstractBrowser, StatesMixin,
APIBrowser, OAuth2Mixin, OAuth2PKCEMixin, TwoFactorBrowser,
)
from .url import URL
__all__ = ['Browser', 'DomainBrowser', 'UrlNotAllowed', 'PagesBrowser', 'URL',
'LoginBrowser', 'need_login', 'AbstractBrowser', 'StatesMixin',
'APIBrowser', 'OAuth2Mixin', 'OAuth2PKCEMixin', 'TwoFactorBrowser', ]
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/adapters.py 0000664 0000000 0000000 00000002465 13726205233 0025427 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2019 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
import requests
__all__ = ['HTTPAdapter']
class HTTPAdapter(requests.adapters.HTTPAdapter):
def __init__(self, *args, **kwargs):
self._proxy_headers = kwargs.pop('proxy_headers', {})
super(HTTPAdapter, self).__init__(*args, **kwargs)
def add_proxy_header(self, key, value):
self._proxy_headers[key] = value
def update_proxy_headers(self, headers):
self._proxy_headers.update(headers)
def proxy_headers(self, proxy):
headers = super(HTTPAdapter, self).proxy_headers(proxy)
headers.update(self._proxy_headers)
return headers
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/browsers.py 0000664 0000000 0000000 00000145451 13726205233 0025475 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012-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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import absolute_import, print_function
from collections import OrderedDict
from functools import wraps
import re
import pickle
import base64
import io
from hashlib import sha256
import zlib
from functools import reduce
try:
from requests.packages import urllib3
except ImportError:
import urllib3
import os
import sys
from copy import deepcopy
import inspect
from datetime import datetime, timedelta
from dateutil import parser
from threading import Lock
try:
import requests
if int(requests.__version__.split('.')[0]) < 2:
raise ImportError()
except ImportError:
raise ImportError('Please install python3-requests >= 2.0')
from weboob.exceptions import (
BrowserHTTPSDowngrade, ModuleInstallError, BrowserRedirect, BrowserIncorrectPassword,
NeedInteractiveFor2FA
)
from weboob.tools.log import getLogger
from weboob.tools.compat import basestring, unicode, urlparse, urljoin, urlencode, parse_qsl
from weboob.tools.misc import to_unicode
from weboob.tools.json import json
from weboob.tools.value import Value
from weboob import __version__
from .adapters import HTTPAdapter
from .cookies import WeboobCookieJar
from .exceptions import HTTPNotFound, ClientError, ServerError
from .sessions import FuturesSession
from .profiles import Firefox
from .pages import NextPage
from .url import URL, normalize_url
class Browser(object):
"""
Simple browser class.
Act like a browser, and don't try to do too much.
"""
PROFILE = Firefox()
"""
Default profile used by browser to navigate on websites.
"""
TIMEOUT = 10.0
"""
Default timeout during requests.
"""
REFRESH_MAX = 0.0
"""
When handling a Refresh header, the browsers considers it only if the sleep
time in lesser than this value.
"""
VERIFY = True
"""
Check SSL certificates.
"""
MAX_RETRIES = 2
"""
Maximum retries on failed requests.
"""
MAX_WORKERS = 10
"""
Maximum of threads for asynchronous requests.
"""
ALLOW_REFERRER = True
"""
Controls the behavior of get_referrer.
"""
COOKIE_POLICY = None
"""
Default CookieJar policy.
Example: weboob.browser.cookies.BlockAllCookies()
"""
@classmethod
def asset(cls, localfile):
"""
Absolute file path for a module local file.
"""
if os.path.isabs(localfile):
return localfile
return os.path.join(os.path.dirname(inspect.getfile(cls)), localfile)
def __new__(cls, *args, **kwargs):
""" Accept any arguments, necessary for AbstractBrowser __new__ override.
AbstractBrowser, in its overridden __new__, removes itself from class hierarchy
so its __new__ is called only once. In python 3, default (object) __new__ is
then used for next instantiations but it's a slot/"fixed" version supporting
only one argument (type to instanciate).
"""
return object.__new__(cls)
def __init__(self, logger=None, proxy=None, responses_dirname=None, weboob=None, proxy_headers=None):
self.logger = getLogger('browser', logger)
self.responses_dirname = responses_dirname
self.responses_count = 1
self.responses_lock = Lock()
if isinstance(self.VERIFY, basestring):
self.VERIFY = self.asset(self.VERIFY)
self.PROXIES = proxy
self.proxy_headers = proxy_headers or {}
self._setup_session(self.PROFILE)
self.url = None
self.response = None
self.har_bundle = None
def deinit(self):
self.session.close()
def set_normalized_url(self, response, **kwargs):
response.url = normalize_url(response.url)
def save_response(self, response, warning=False, **kwargs):
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)
import mimetypes
# get the content-type, remove optionnal charset part
mimetype = response.headers.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 ''
with self.responses_lock:
counter = self.responses_count
self.responses_count += 1
path = re.sub(r'[^A-z0-9\.-_]+', '_', urlparse(response.url).path.rpartition('/')[2])[-10:]
if path.endswith(ext):
ext = ''
filename = '%02d-%d%s%s%s' % \
(counter, response.status_code, '-' if path else '', path, ext)
response_filepath = os.path.join(self.responses_dirname, filename)
request = response.request
with open(response_filepath + '-request.txt', 'w') as f:
f.write('%s %s\n\n\n' % (request.method, request.url))
for key, value in request.headers.items():
f.write('%s: %s\n' % (key, value))
if request.body is not None: # separate '' from None
f.write('\n\n\n%s' % request.body)
with open(response_filepath + '-response.txt', 'w') as f:
if hasattr(response.elapsed, 'total_seconds'):
f.write('Time: %3.3fs\n' % response.elapsed.total_seconds())
f.write('%s %s\n\n\n' % (response.status_code, response.reason))
for key, value in response.headers.items():
f.write('%s: %s\n' % (key, value))
with open(response_filepath, 'wb') as f:
f.write(response.content)
match_filepath = os.path.join(self.responses_dirname, 'url_response_match.txt')
with open(match_filepath, 'a') as f:
f.write('# %d %s %s\n' % (response.status_code, response.reason, response.headers.get('Content-Type', '')))
f.write('%s\t%s\n' % (response.url, filename))
request = response.request
if not self.har_bundle:
self.har_bundle = {
'log': {
'version': '1.2',
'creator': {
'name': 'weboob',
'version': __version__,
},
'browser': {
'name': 'weboob',
'version': __version__,
},
# there are no pages, but we need that to please firefox
'pages': [{
'id': 'fake_page',
'pageTimings': {},
# and chromium wants some of it too
'startedDateTime': (datetime.now() - response.elapsed).isoformat(),
}],
# don't put additional data after this list, to have a fixed-size suffix after it
# so we can add more entries without rewriting the whole file.
'entries': [],
},
}
har_entry = {
'startedDateTime': (datetime.now() - response.elapsed).isoformat(),
'pageref': 'fake_page',
'time': int(response.elapsed.total_seconds() * 1000),
'request': {
'method': request.method,
'url': request.url,
'httpVersion': 'HTTP/%.1f' % (response.raw.version / 10.),
'headers': [
{
'name': k,
'value': v,
}
for k, v in request.headers.items()
],
'queryString': [
{
'name': key,
'value': value,
}
for key, value in parse_qsl(
urlparse(request.url).query,
keep_blank_values=True,
)
],
'cookies': [
{
'name': k,
'value': v,
}
for k, v in request._cookies.items()
],
# for chromium
'bodySize': -1,
'headersSize': -1,
},
'response': {
'status': response.status_code,
'statusText': to_unicode(response.reason),
'httpVersion': 'HTTP/%.1f' % (response.raw.version / 10.),
'headers': [
{
'name': k,
'value': v,
}
for k, v in response.headers.items()
],
'content': {
'mimeType': response.headers.get('Content-Type', ''),
'size': len(response.content),
'encoding': "base64",
'text': base64.b64encode(response.content).decode('ascii'),
},
'cookies': [
{
'name': k,
'value': v,
}
for k, v in response.cookies.items()
],
'redirectURL': response.headers.get('location', ''),
# for chromium
'bodySize': -1,
'headersSize': -1,
},
'timings': { # please chromium
'send': -1,
'wait': -1,
'receive': -1,
},
'cache': {},
}
if request.body is not None:
har_entry['request']['postData'] = {
'mimeType': request.headers.get('Content-Type', ''),
'params': [],
}
if isinstance(request.body, str):
har_entry['request']['postData']['text'] = request.body
else:
har_entry['request']['postData']['text'] = request.body.decode('latin-1')
if request.headers.get('Content-Type') == 'application/x-www-form-urlencoded':
har_entry['request']['postData']['params'] = [
{
"name": key,
"value": value,
} for key, value in parse_qsl(request.body)
]
with self.responses_lock:
self.har_bundle['log']['entries'].append(har_entry)
har_path = os.path.join(self.responses_dirname, 'bundle.har')
if not os.path.isfile(har_path):
with open(har_path, 'w') as fd:
json.dump(self.har_bundle, fd, separators=(',', ':'))
else:
# hack to avoid rewriting the whole file: entries are last in the JSON file
# we need to seek at the right place and write the new entry.
# this will unfortunately overwrite closings.
suffix = "]}}"
with open(har_path, 'r+') as fd:
# can't seek with a negative value...
fd.seek(0, io.SEEK_END)
after_entry_pos = fd.tell() - len(suffix)
fd.seek(after_entry_pos)
if fd.read(len(suffix)) != suffix:
self.logger.warning('HAR file does not end with the expected pattern')
else:
fd.seek(after_entry_pos)
fd.write(',') # there should have been at least one entry
json.dump(har_entry, fd, separators=(',', ':'))
fd.write(suffix)
msg = u'Response saved to %s' % response_filepath
if warning:
self.logger.warning(msg)
else:
self.logger.info(msg)
def _create_session(self):
return FuturesSession(max_workers=self.MAX_WORKERS, max_retries=self.MAX_RETRIES)
def _setup_session(self, profile):
"""
Set up a python3-requests session for our usage.
"""
session = self._create_session()
session.proxies = self.PROXIES
session.verify = not self.logger.settings['ssl_insecure'] and self.VERIFY
if not session.verify:
try:
urllib3.disable_warnings()
except AttributeError:
# urllib3 is too old, warnings won't be disable
pass
# defines a max_retries. It's mandatory in case a server is not
# handling keep alive correctly, like the proxy burp
adapter_kwargs = dict(max_retries=self.MAX_RETRIES,
proxy_headers=self.proxy_headers)
# set connection pool size equal to MAX_WORKERS if needed
if self.MAX_WORKERS > requests.adapters.DEFAULT_POOLSIZE:
adapter_kwargs.update(pool_connections=self.MAX_WORKERS,
pool_maxsize=self.MAX_WORKERS)
session.mount('https://', HTTPAdapter(**adapter_kwargs))
session.mount('http://', HTTPAdapter(**adapter_kwargs))
if self.TIMEOUT:
session.timeout = self.TIMEOUT
## weboob only can provide proxy and HTTP auth options
session.trust_env = False
profile.setup_session(session)
session.hooks['response'].append(self.set_normalized_url)
if self.responses_dirname is not None:
session.hooks['response'].append(self.save_response)
self.session = session
session.cookies = WeboobCookieJar()
if self.COOKIE_POLICY:
session.cookies.set_policy(self.COOKIE_POLICY)
def set_profile(self, profile):
profile.setup_session(self.session)
def location(self, url, **kwargs):
"""
Like :meth:`open` but also changes the current URL and response.
This is the most common method to request web pages.
Other than that, has the exact same behavior of open().
"""
assert not kwargs.get('is_async'), "Please use open() instead of location() to make asynchronous requests."
response = self.open(url, **kwargs)
self.response = response
self.url = self.response.url
return response
def open(self, url, referrer=None,
allow_redirects=True,
stream=None,
timeout=None,
verify=None,
cert=None,
proxies=None,
data_encoding=None,
is_async=False,
callback=lambda response: response,
**kwargs):
"""
Make an HTTP request like a browser does:
* follow redirects (unless disabled)
* provide referrers (unless disabled)
Unless a `method` is explicitly provided, it makes a GET request,
or a POST if data is not None,
An empty `data` (not None, like '' or {}) *will* make a POST.
It is a wrapper around session.request().
All session.request() options are available.
You should use location() or open() and not session.request(),
since it has some interesting additions, which are easily
individually disabled through the arguments.
Call this instead of location() if you do not want to "visit" the URL
(for instance, you are downloading a file).
When `is_async` is True, open() returns a Future object (see
concurrent.futures for more details), which can be evaluated with its
result() method. If any exception is raised while processing request,
it is caught and re-raised when calling result().
For example:
>>> Browser().open('http://google.com', is_async=True).result().text # doctest: +SKIP
:param url: URL
:type url: str
:param data: POST data
:type url: str or dict or None
:param referrer: Force referrer. False to disable sending it, None for guessing
:type referrer: str or False or None
:param is_async: Process request in a non-blocking way
:type is_async: bool
:param callback: Callback to be called when request has finished,
with response as its first and only argument
:type callback: function
:rtype: :class:`requests.Response`
"""
if 'async' in kwargs:
import warnings
warnings.warn('Please use is_async instead of async.', DeprecationWarning)
is_async = kwargs['async']
del kwargs['async']
if isinstance(url, basestring):
url = normalize_url(url)
elif isinstance(url, requests.Request):
url.url = normalize_url(url.url)
req = self.build_request(url, referrer, data_encoding=data_encoding, **kwargs)
preq = self.prepare_request(req)
if hasattr(preq, '_cookies'):
# The _cookies attribute is not present in requests < 2.2. As in
# previous version it doesn't calls extract_cookies_to_jar(), it is
# not a problem as we keep our own cookiejar instance.
preq._cookies = WeboobCookieJar.from_cookiejar(preq._cookies)
if self.COOKIE_POLICY:
preq._cookies.set_policy(self.COOKIE_POLICY)
if proxies is None:
proxies = self.PROXIES
if verify is None:
verify = not self.logger.settings['ssl_insecure'] and self.VERIFY
if timeout is None:
timeout = self.TIMEOUT
# We define an inner_callback here in order to execute the same code
# regardless of is_async param.
def inner_callback(future, response):
if allow_redirects:
response = self.handle_refresh(response)
self.raise_for_status(response)
return callback(response)
# call python3-requests
response = self.session.send(preq,
allow_redirects=allow_redirects,
stream=stream,
timeout=timeout,
verify=verify,
cert=cert,
proxies=proxies,
callback=inner_callback,
is_async=is_async)
return response
def async_open(self, url, **kwargs):
"""
Shortcut to open(url, is_async=True).
"""
if 'async' in kwargs:
del kwargs['async']
if 'is_async' in kwargs:
del kwargs['is_async']
return self.open(url, is_async=True, **kwargs)
def raise_for_status(self, response):
"""
Like Response.raise_for_status but will use other classes if needed.
"""
http_error_msg = None
if 400 <= response.status_code < 500:
http_error_msg = '%s Client Error: %s' % (response.status_code, response.reason)
cls = ClientError
if response.status_code == 404:
cls = HTTPNotFound
elif 500 <= response.status_code < 600:
http_error_msg = '%s Server Error: %s' % (response.status_code, response.reason)
cls = ServerError
if http_error_msg:
raise cls(http_error_msg, response=response)
# in case we did not catch something that should be
response.raise_for_status()
def build_request(self, url, referrer=None, data_encoding=None, **kwargs):
"""
Does the same job as open(), but returns a Request without
submitting it.
This allows further customization to the Request.
"""
if isinstance(url, requests.Request):
req = url
url = req.url
else:
req = requests.Request(url=url, **kwargs)
# guess method
if req.method is None:
# 'data' and 'json' (even if empty) are (always?) passed to build_request
# and None is their default. For a Request object, the defaults are different.
# Request.json is None and Request.data == [] by default.
# Could they break unexpectedly?
if (
req.data or kwargs.get('data') is not None
or req.json or kwargs.get('json') is not None
):
req.method = 'POST'
else:
req.method = 'GET'
# convert unicode strings to proper encoding
if isinstance(req.data, unicode) and data_encoding:
req.data = req.data.encode(data_encoding)
if isinstance(req.data, dict) and data_encoding:
req.data = OrderedDict([(k, v.encode(data_encoding) if isinstance(v, unicode) else v)
for k, v in req.data.items()])
if referrer is None:
referrer = self.get_referrer(self.url, url)
if referrer:
# Yes, it is a misspelling.
req.headers.setdefault('Referer', referrer)
return req
def prepare_request(self, req):
"""
Get a prepared request from a Request object.
This method aims to be overloaded by children classes.
"""
return self.session.prepare_request(req)
REFRESH_RE = re.compile(r"^(?P[\d\.]+)(;\s*url=[\"']?(?P.*?)[\"']?)?$", re.IGNORECASE)
def handle_refresh(self, response):
"""
Called by open, to handle Refresh HTTP header.
It only redirect to the refresh URL if the sleep time is inferior to
REFRESH_MAX.
"""
if 'Refresh' not in response.headers:
return response
m = self.REFRESH_RE.match(response.headers['Refresh'])
if m:
# XXX perhaps we should not redirect if the refresh url is equal to the current url.
url = m.groupdict().get('url', None) or response.request.url
sleep = float(m.groupdict()['sleep'])
if sleep <= self.REFRESH_MAX:
self.logger.debug('Refresh to %s' % url)
return self.open(url)
else:
self.logger.debug('Do not refresh to %s because %s > REFRESH_MAX(%s)' % (url, sleep, self.REFRESH_MAX))
return response
self.logger.warning('Unable to handle refresh "%s"' % response.headers['Refresh'])
return response
def get_referrer(self, oldurl, newurl):
"""
Get the referrer to send when doing a request.
If we should not send a referrer, it will return None.
Reference: https://en.wikipedia.org/wiki/HTTP_referer
The behavior can be controlled through the ALLOW_REFERRER attribute.
True always allows the referers
to be sent, False never, and None only if it is within
the same domain.
:param oldurl: Current absolute URL
:type oldurl: str or None
:param newurl: Target absolute URL
:type newurl: str
:rtype: str or None
"""
if self.ALLOW_REFERRER is False:
return
if oldurl is None:
return
old = urlparse(oldurl)
new = urlparse(newurl)
# Do not leak secure URLs to insecure URLs
if old.scheme == 'https' and new.scheme != 'https':
return
# Reloading the page. Usually no referrer.
if oldurl == newurl:
return
# Domain-based privacy
if self.ALLOW_REFERRER is None and old.netloc != new.netloc:
return
return oldurl
def export_session(self):
def make_cookie(c):
d = {
k: getattr(c, k) for k in ['name', 'value', 'domain', 'path', 'secure']
}
#d['session'] = c.discard
d['httpOnly'] = 'httponly' in [k.lower() for k in c._rest.keys()]
d['expirationDate'] = getattr(c, 'expires', None)
return d
return {
'url': self.url,
'cookies': [make_cookie(c) for c in self.session.cookies],
}
class UrlNotAllowed(Exception):
"""
Raises by :class:`DomainBrowser` when `RESTRICT_URL` is set and trying to go
on an url not matching `BASEURL`.
"""
class DomainBrowser(Browser):
"""
A browser that handles relative URLs and can have a base URL (usually a domain).
For instance self.location('/hello') will get http://weboob.org/hello
if BASEURL is 'http://weboob.org/'.
"""
BASEURL = None
"""
Base URL, e.g. 'http://weboob.org/' or 'https://weboob.org/'
See absurl().
"""
RESTRICT_URL = False
"""
URLs allowed to load.
This can be used to force SSL (if the BASEURL is SSL) or any other leakage.
Set to True to allow only URLs starting by the BASEURL.
Set it to a list of allowed URLs if you have multiple allowed URLs.
More complex behavior is possible by overloading url_allowed()
"""
def __init__(self, baseurl=None, *args, **kwargs):
super(DomainBrowser, self).__init__(*args, **kwargs)
if baseurl is not None:
self.BASEURL = baseurl
def url_allowed(self, url):
"""
Checks if we are allowed to visit an URL.
See RESTRICT_URL.
:param url: Absolute URL
:type url: str
:rtype: bool
"""
if self.BASEURL is None or self.RESTRICT_URL is False:
return True
if self.RESTRICT_URL is True:
return url.startswith(self.BASEURL)
for restrict_url in self.RESTRICT_URL:
if url.startswith(restrict_url):
return True
return False
def absurl(self, uri, base=None):
"""
Get the absolute URL, relative to a base URL.
If base is None, it will try to use the current URL.
If there is no current URL, it will try to use BASEURL.
If base is False, it will always try to use the current URL.
If base is True, it will always try to use BASEURL.
:param uri: URI to make absolute. It can be already absolute.
:type uri: str
:param base: Base absolute URL.
:type base: str or None or False or True
:rtype: str
"""
if not base:
base = self.url
if base is None or base is True:
base = self.BASEURL
return urljoin(base, uri)
def open(self, req, *args, **kwargs):
"""
Like :meth:`Browser.open` but handles urls without domains, using
the :attr:`BASEURL` attribute.
"""
uri = req.url if isinstance(req, requests.Request) else req
url = self.absurl(uri)
if not self.url_allowed(url):
raise UrlNotAllowed(url)
if isinstance(req, requests.Request):
req.url = url
else:
req = url
return super(DomainBrowser, self).open(req, *args, **kwargs)
def go_home(self):
"""
Go to the "home" page, usually the BASEURL.
"""
return self.location(self.BASEURL or self.absurl('/'))
class PagesBrowser(DomainBrowser):
r"""
A browser which works pages and keep state of navigation.
To use it, you have to derive it and to create URL objects as class
attributes. When open() or location() are called, if the url matches
one of URL objects, it returns a Page object. In case of location(), it
stores it in self.page.
Example:
>>> from .pages import HTMLPage
>>> class ListPage(HTMLPage):
... def get_items():
... return [el.attrib['id'] for el in self.doc.xpath('//div[@id="items"]/div')]
...
>>> class ItemPage(HTMLPage):
... pass
...
>>> class MyBrowser(PagesBrowser):
... BASEURL = 'http://example.org/'
... list = URL('list-items', ListPage)
... item = URL('item/view/(?P\d+)', ItemPage)
...
>>> MyBrowser().list.stay_or_go().get_items() # doctest: +SKIP
>>> bool(MyBrowser().list.match('http://example.org/list-items'))
True
>>> bool(MyBrowser().list.match('http://example.org/'))
False
>>> str(MyBrowser().item.build(id=42))
'http://example.org/item/view/42'
You can then use URL instances to go on pages.
"""
_urls = None
def __init__(self, *args, **kwargs):
self.highlight_el = kwargs.pop('highlight_el', False)
super(PagesBrowser, self).__init__(*args, **kwargs)
self.page = None
# exclude properties because they can access other fields not yet defined
def is_property(attr):
v = getattr(type(self), attr, None)
return hasattr(v, '__get__') or hasattr(v, '__set__')
attrs = [(attr, getattr(self, attr)) for attr in dir(self) if not is_property(attr)]
attrs = [v for v in attrs if isinstance(v[1], URL)]
attrs.sort(key=lambda v: v[1]._creation_counter)
self._urls = OrderedDict(deepcopy(attrs))
for k, v in self._urls.items():
setattr(self, k, v)
for url in self._urls.values():
url.browser = self
def open(self, *args, **kwargs):
"""
Same method than
:meth:`weboob.browser.browsers.DomainBrowser.open`, but the
response contains an attribute `page` if the url matches any
:class:`URL` object.
"""
callback = kwargs.pop('callback', lambda response: response)
page_class = kwargs.pop('page', None)
# Have to define a callback to seamlessly process synchronous and
# asynchronous requests, see :meth:`Browser.open` and its `is_async`
# and `callback` params.
def internal_callback(response):
# Try to handle the response page with an URL instance.
response.page = None
if page_class:
response.page = page_class(self, response)
return callback(response)
for url in self._urls.values():
response.page = url.handle(response)
if response.page is not None:
self.logger.debug('Handle %s with %s', response.url, response.page.__class__.__name__)
break
if response.page is None:
regexp = r'^(?P\w+)://.*'
proto_response = re.match(regexp, response.url)
if proto_response and self.BASEURL:
proto_response = proto_response.group('proto')
proto_base = re.match(regexp, self.BASEURL).group('proto')
if proto_base == 'https' and proto_response != 'https':
raise BrowserHTTPSDowngrade()
self.logger.debug('Unable to handle %s', response.url)
return callback(response)
return super(PagesBrowser, self).open(callback=internal_callback, *args, **kwargs)
def location(self, *args, **kwargs):
"""
Same method than
:meth:`weboob.browser.browsers.Browser.location`, but if the
url matches any :class:`URL` object, an attribute `page` is added to
response, and the attribute :attr:`PagesBrowser.page` is set.
"""
if self.page is not None:
# Call leave hook.
self.page.on_leave()
response = self.open(*args, **kwargs)
self.response = response
self.page = response.page
self.url = response.url
if self.page is not None:
# Call load hook.
self.page.on_load()
# Returns self.response in case on_load recalls location()
return self.response
def pagination(self, func, *args, **kwargs):
r"""
This helper function can be used to handle pagination pages easily.
When the called function raises an exception :class:`NextPage`, it goes
on the wanted page and recall the function.
:class:`NextPage` constructor can take an url or a Request object.
>>> from .pages import HTMLPage
>>> class Page(HTMLPage):
... def iter_values(self):
... for el in self.doc.xpath('//li'):
... yield el.text
... for next in self.doc.xpath('//a'):
... raise NextPage(next.attrib['href'])
...
>>> class Browser(PagesBrowser):
... BASEURL = 'https://weboob.org'
... list = URL('/tests/list-(?P\d+).html', Page)
...
>>> b = Browser()
>>> b.list.go(pagenum=1) # doctest: +ELLIPSIS
>>> list(b.pagination(lambda: b.page.iter_values()))
['One', 'Two', 'Three', 'Four']
"""
while True:
try:
for r in func(*args, **kwargs):
yield r
except NextPage as e:
self.location(e.request)
else:
return
def need_login(func):
"""
Decorator used to require to be logged to access to this function.
This decorator can be used on any method whose first argument is a
browser (typically a :class:`LoginBrowser`). It checks for the
`logged` attribute in the current browser's page: when this
attribute is set to ``True`` (e.g., when the page inherits
:class:`LoggedPage`), then nothing special happens.
In all other cases (when the browser isn't on any defined page or
when the page's `logged` attribute is ``False``), the
:meth:`LoginBrowser.do_login` method of the browser is called before
calling :`func`.
"""
@wraps(func)
def inner(browser, *args, **kwargs):
if (not hasattr(browser, 'logged') or (hasattr(browser, 'logged') and not browser.logged)) and \
(not hasattr(browser, 'page') or browser.page is None or not browser.page.logged):
browser.do_login()
if browser.logger.settings.get('export_session'):
browser.logger.debug('logged in with session: %s', json.dumps(browser.export_session()))
return func(browser, *args, **kwargs)
return inner
class LoginBrowser(PagesBrowser):
"""
A browser which supports login.
"""
def __init__(self, username, password, *args, **kwargs):
super(LoginBrowser, self).__init__(*args, **kwargs)
self.username = username
self.password = password
def do_login(self):
"""
Abstract method to implement to login on website.
It is called when a login is needed.
"""
raise NotImplementedError()
def do_logout(self):
"""
Logout from website.
By default, simply clears the cookies.
"""
self.session.cookies.clear()
class StatesMixin(object):
"""
Mixin to store states of browser.
"""
__states__ = []
"""
Saved state variables.
"""
STATE_DURATION = None
"""
In minutes, used to set an expiration datetime object of the state.
"""
def locate_browser(self, state):
try:
self.location(state['url'])
except (requests.exceptions.HTTPError, requests.exceptions.TooManyRedirects):
pass
def _load_cookies(self, cookie_state):
try:
uncompressed = zlib.decompress(base64.b64decode(cookie_state))
except (TypeError, zlib.error, EOFError, ValueError):
self.logger.error('Unable to uncompress cookies from storage')
return
try:
jcookies = json.loads(uncompressed)
except ValueError:
try:
self.session.cookies = pickle.loads(uncompressed)
except (TypeError, EOFError, ValueError):
self.logger.error('Unable to reload cookies from storage')
else:
self.logger.warning('Reloaded deprecated cookie format')
else:
for jcookie in jcookies:
self.session.cookies.set(**jcookie)
self.logger.info('Reloaded cookies from storage')
def load_state(self, state):
if state.get('expire') and parser.parse(state['expire']) < datetime.now():
return self.logger.info('State expired, not reloading it from storage')
if 'cookies' in state:
self._load_cookies(state['cookies'])
for attrname in self.__states__:
if attrname in state:
setattr(self, attrname, state[attrname])
if 'url' in state:
self.locate_browser(state)
def get_expire(self):
return unicode((datetime.now() + timedelta(minutes=self.STATE_DURATION)).replace(microsecond=0))
def dump_state(self):
state = {}
if hasattr(self, 'page') and self.page:
state['url'] = self.page.url
cookies = [
{
attr: getattr(cookie, attr)
for attr in ('name', 'value', 'domain', 'path', 'secure', 'expires')
}
for cookie in self.session.cookies
]
state['cookies'] = base64.b64encode(zlib.compress(json.dumps(cookies).encode('utf-8'))).decode('ascii')
for attrname in self.__states__:
try:
state[attrname] = getattr(self, attrname)
except AttributeError:
pass
if self.STATE_DURATION is not None:
state['expire'] = self.get_expire()
self.logger.info('Stored cookies into storage')
return state
class APIBrowser(DomainBrowser):
"""
A browser for API websites.
"""
def build_request(self, *args, **kwargs):
if 'data' in kwargs:
kwargs['data'] = json.dumps(kwargs['data'])
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['Content-Type'] = 'application/json'
return super(APIBrowser, self).build_request(*args, **kwargs)
def open(self, *args, **kwargs):
"""
Do a JSON request.
The "Content-Type" header is always set to "application/json".
:param data: if specified, format as JSON and send as request body
:type data: :class:`dict`
:param headers: if specified, add these headers to the request
:type headers: :class:`dict`
"""
return super(APIBrowser, self).open(*args, **kwargs)
def request(self, *args, **kwargs):
"""
Do a JSON request and parse the response.
:returns: a dict containing the parsed JSON server response
:rtype: :class:`dict`
"""
return self.open(*args, **kwargs).json()
class AbstractBrowserMissingParentError(Exception):
pass
class AbstractBrowser(Browser):
""" AbstractBrowser allow inheritance of a browser defined in another module.
Websites can share many pages and code base. This class allow to load a browser
provided by another module and to build our own browser on top of it (like standard
python inheritance. Weboob will install and download the PARENT module for you.
PARENT is a mandatory attribute, it's the name of the module providing the parent Browser
PARENT_ATTR is an optional attribute used when the parent module does not have only one
browser defined as BROWSER class attribute: you can customized the path of the object to load.
Note that you must pass a valid weboob instance as first argument of the constructor.
"""
PARENT = None
PARENT_ATTR = None
@classmethod
def _resolve_abstract(cls, weboob):
if cls.PARENT is None:
raise AbstractBrowserMissingParentError("PARENT is not defined for browser %s" % cls)
try:
module = weboob.load_or_install_module(cls.PARENT)
except ModuleInstallError as err:
raise ModuleInstallError('This module depends on %s module but %s\'s installation failed with: %s' % (cls.PARENT, cls.PARENT, err))
if cls.PARENT_ATTR is None:
parent = module.klass.BROWSER
else:
parent = reduce(getattr, cls.PARENT_ATTR.split('.'), module)
if parent is None:
raise AbstractBrowserMissingParentError("Failed to load parent class")
# Parent may be an AbstractBrowser as well
if hasattr(parent, '_resolve_abstract'):
parent._resolve_abstract(weboob)
cls.__bases__ = (parent,)
cls.weboob = weboob
def __new__(cls, *args, **kwargs):
weboob = kwargs['weboob']
cls._resolve_abstract(weboob)
return Browser.__new__(cls, *args, **kwargs)
class OAuth2Mixin(StatesMixin):
AUTHORIZATION_URI = None
ACCESS_TOKEN_URI = None
SCOPE = ''
client_id = None
client_secret = None
redirect_uri = None
access_token = None
access_token_expire = None
auth_uri = None
token_type = None
refresh_token = None
oauth_state = None
def __init__(self, *args, **kwargs):
super(OAuth2Mixin, self).__init__(*args, **kwargs)
self.__states__ += ('access_token', 'access_token_expire', 'refresh_token', 'token_type')
def build_request(self, *args, **kwargs):
headers = kwargs.setdefault('headers', {})
if self.access_token:
headers['Authorization'] = '{} {}'.format(self.token_type, self.access_token)
return super(OAuth2Mixin, self).build_request(*args, **kwargs)
def dump_state(self):
self.access_token_expire = unicode(self.access_token_expire) if self.access_token_expire else None
return super(OAuth2Mixin, self).dump_state()
def load_state(self, state):
super(OAuth2Mixin, self).load_state(state)
self.access_token_expire = parser.parse(self.access_token_expire) if self.access_token_expire else None
def raise_for_status(self, response):
if response.status_code == 401:
self.access_token = None
return super(OAuth2Mixin, self).raise_for_status(response)
@property
def logged(self):
return self.access_token is not None and (not self.access_token_expire or self.access_token_expire > datetime.now())
def do_login(self):
if self.refresh_token:
self.use_refresh_token()
elif self.auth_uri:
self.request_access_token(self.auth_uri)
else:
self.request_authorization()
def build_authorization_parameters(self):
params = {
'redirect_uri': self.redirect_uri,
'scope': self.SCOPE,
'client_id': self.client_id,
'response_type': 'code',
}
if self.oauth_state:
params['state'] = self.oauth_state
return params
def build_authorization_uri(self):
p = urlparse(self.AUTHORIZATION_URI)
q = dict(parse_qsl(p.query))
q.update(self.build_authorization_parameters())
return p._replace(query=urlencode(q)).geturl()
def request_authorization(self):
self.logger.info('request authorization')
raise BrowserRedirect(self.build_authorization_uri())
def handle_callback_error(self, values):
# Here we try to catch callback errors occurring during enrollment
# Ideally overload this method in each module to catch specific error
assert values.get('code'), "No 'code' was found into the callback url, please raise the right error: %s" % values
def build_access_token_parameters(self, values):
return {'code': values['code'],
'grant_type': 'authorization_code',
'redirect_uri': self.redirect_uri,
'client_id': self.client_id,
'client_secret': self.client_secret,
}
def do_token_request(self, data):
return self.open(self.ACCESS_TOKEN_URI, data=data)
def request_access_token(self, auth_uri):
self.logger.info('requesting access token')
if isinstance(auth_uri, dict):
values = auth_uri
else:
values = dict(parse_qsl(urlparse(auth_uri).query))
self.handle_callback_error(values)
data = self.build_access_token_parameters(values)
try:
auth_response = self.do_token_request(data).json()
except ClientError:
raise BrowserIncorrectPassword()
self.update_token(auth_response)
def build_refresh_token_parameters(self):
return {
'grant_type': 'refresh_token',
'refresh_token': self.refresh_token,
'client_id': self.client_id,
'client_secret': self.client_secret,
'redirect_uri': self.redirect_uri,
}
def use_refresh_token(self):
self.logger.info('refreshing token')
data = self.build_refresh_token_parameters()
try:
auth_response = self.do_token_request(data).json()
except ClientError:
self.refresh_token = None
raise BrowserIncorrectPassword()
self.update_token(auth_response)
def update_token(self, auth_response):
self.token_type = auth_response.get('token_type', 'Bearer').capitalize() # don't know yet if this is a good idea, but required by bnpstet
if 'refresh_token' in auth_response:
self.refresh_token = auth_response['refresh_token']
self.access_token = auth_response['access_token']
self.access_token_expire = datetime.now() + timedelta(seconds=int(auth_response['expires_in']))
class OAuth2PKCEMixin(OAuth2Mixin):
def __init__(self, *args, **kwargs):
super(OAuth2PKCEMixin, self).__init__(*args, **kwargs)
self.__states__ += ('pkce_verifier', 'pkce_challenge')
self.pkce_verifier = self.code_verifier()
self.pkce_challenge = self.code_challenge(self.pkce_verifier)
# PKCE (Proof Key for Code Exchange) standard protocol methods:
def code_verifier(self, bytes_number=64):
return base64.urlsafe_b64encode(os.urandom(bytes_number)).rstrip(b'=').decode('ascii')
def code_challenge(self, verifier):
digest = sha256(verifier.encode('utf8')).digest()
return base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
def build_authorization_parameters(self):
params = {
'redirect_uri': self.redirect_uri,
'code_challenge_method': 'S256',
'code_challenge': self.pkce_challenge,
'client_id': self.client_id,
}
if self.oauth_state:
params['state'] = self.oauth_state
return params
def build_access_token_parameters(self, values):
return {'code': values['code'],
'grant_type': 'authorization_code',
'code_verifier': self.pkce_verifier,
'redirect_uri': self.redirect_uri,
'client_id': self.client_id,
'client_secret': self.client_secret,
}
class TwoFactorBrowser(LoginBrowser, StatesMixin):
# period to keep the same state
# it is different from STATE_DURATION which updates the expire date at each dump
TWOFA_DURATION = None
INTERACTIVE_NAME = 'request_information'
# dict of config keys and methods used for double authentication
# must be set up in the init to handle function pointers
AUTHENTICATION_METHODS = {}
# list of cookie keys to clear before dumping state
COOKIES_TO_CLEAR = ()
# login can also be done with credentials without 2FA
HAS_CREDENTIALS_ONLY = False
def __init__(self, config, *args, **kwargs):
super(TwoFactorBrowser, self).__init__(*args, **kwargs)
self.config = config
self.is_interactive = config.get(self.INTERACTIVE_NAME, Value()).get() is not None
self.twofa_logged_date = None
self.__states__ += ('twofa_logged_date',)
def get_expire(self):
if self.twofa_logged_date:
logged_date = parser.parse(self.twofa_logged_date)
else:
logged_date = None
expires_dates = [datetime.now() + timedelta(minutes=self.STATE_DURATION)]
if logged_date and self.TWOFA_DURATION is not None:
expires_dates.append(logged_date + timedelta(minutes=self.TWOFA_DURATION))
return unicode(max(expires_dates).replace(microsecond=0))
def dump_state(self):
self.clear_not_2fa_cookies()
# so the date can be parsed in json
# because twofa_logged_date is in state
if self.twofa_logged_date:
self.twofa_logged_date = str(self.twofa_logged_date)
return super(TwoFactorBrowser, self).dump_state()
def init_login(self):
"""
Abstract method to implement initiation of login on website.
This method should raise an exception.
SCA exceptions :
- AppValidation for polling method
- BrowserQuestion for SMS method, token method etc.
Any other exceptions, default to BrowserIncorrectPassword.
"""
raise NotImplementedError()
def clear_init_cookies(self):
# clear cookies to avoid some errors
self.session.cookies.clear()
def clear_not_2fa_cookies(self):
# clear cookies that we don't need for 2FA
for cookie_key in self.COOKIES_TO_CLEAR:
if cookie_key in self.session.cookies:
del self.session.cookies[cookie_key]
def check_interactive(self):
if not self.is_interactive:
raise NeedInteractiveFor2FA()
def do_double_authentication(self):
"""
This method will check AUTHENTICATION_METHODS
to dispatch to the right handle_* method.
If no backend configuration could be found,
it will then call init_login method.
"""
assert self.AUTHENTICATION_METHODS, 'There is no config for the double authentication.'
self.twofa_logged_date = None
for config_key, handle_method in self.AUTHENTICATION_METHODS.items():
config_value = self.config.get(config_key, Value())
if not config_value:
continue
setattr(self, config_key, config_value.get())
if getattr(self, config_key):
handle_method()
self.twofa_logged_date = datetime.now()
# cleaning authentication config keys
for config_key in self.AUTHENTICATION_METHODS.keys():
if config_key in self.config:
self.config[config_key] = self.config[config_key].default
break
else:
if not self.HAS_CREDENTIALS_ONLY:
self.check_interactive()
self.clear_init_cookies()
self.init_login()
do_login = do_double_authentication
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/cache.py 0000664 0000000 0000000 00000006637 13726205233 0024674 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2012-2016 weboob project
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
__all__ = ['CacheMixin']
class CacheEntry(object):
def __init__(self, response):
self.response = response
self.etag = response.headers.get('ETag')
self.last_modified = response.headers.get('Last-Modified')
def has_cache_key(self):
return (self.etag or self.last_modified)
def update_request(self, request):
if self.last_modified:
request.headers['If-Modified-Since'] = self.last_modified
if self.etag:
request.headers['If-None-Match'] = self.etag
class CacheMixin(object):
"""Mixin to inherit in a Browser"""
def __init__(self, *args, **kwargs):
super(CacheMixin, self).__init__(*args, **kwargs)
self.cache = {}
"""Cache store object
To limit the size of the cache, a :class:`weboob.tools.lrudict.LimitedLRUDict`
instance can be used.
"""
self.is_updatable = True
"""Whether the cache is updatable
If `False`, once a request has been successfully executed, its response
will always be returned.
If `True`, the `ETag` and `Last-Modified` of the response will be
stored along with the cache. When the request is re-executed, instead
of simply returning the previous response, the server is queried to
check if a newer version of the page exists.
If a newer page exists, it is returned instead and overwrites the
obsolete page in the cache.
"""
def make_cache_key(self, request):
"""Make a key for the cache corresponding to the request."""
body = getattr(request, 'body', None)
headers = tuple(request.headers.values())
return (request.method, request.url, body, headers)
def open_with_cache(self, url, **kwargs):
"""Perform a request using the cache if possible."""
request = self.build_request(url, **kwargs)
key = self.make_cache_key(request)
if key in self.cache:
if not self.is_updatable:
self.logger.debug('cache HIT for %r', request.url)
return self.cache[key].response
else:
self.cache[key].update_request(request)
response = super(CacheMixin, self).open(request, **kwargs)
if response.status_code == 304:
self.logger.debug('cache HIT for %r', request.url)
return self.cache[key].response
elif response.status_code == 200:
entry = CacheEntry(response)
if entry.has_cache_key():
self.logger.debug('storing %r response in cache', request.url)
self.cache[key] = entry
self.logger.debug('cache MISS for %r', request.url)
return response
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/cookies.py 0000664 0000000 0000000 00000003611 13726205233 0025252 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
import requests.cookies
try:
import cookielib
except ImportError:
import http.cookiejar as cookielib
__all__ = ['WeboobCookieJar', 'BlockAllCookies']
class WeboobCookieJar(requests.cookies.RequestsCookieJar):
@classmethod
def from_cookiejar(klass, cj):
"""
Create a WeboobCookieJar from another CookieJar instance.
"""
return requests.cookies.merge_cookies(klass(), cj)
def export(self, filename):
"""
Export all cookies to a file, regardless of expiration, etc.
"""
cj = requests.cookies.merge_cookies(cookielib.LWPCookieJar(), self)
cj.save(filename, ignore_discard=True, ignore_expires=True)
def copy(self):
"""Return an object copy of the cookie jar."""
new_cj = type(self)()
if hasattr(self, 'get_policy'):
new_cj.set_policy(self.get_policy())
else:
new_cj.set_policy(self._policy)
new_cj.update(self)
return new_cj
class BlockAllCookies(cookielib.CookiePolicy):
return_ok = set_ok = domain_return_ok = path_return_ok = lambda self, *args, **kwargs: False
netscape = True
rfc2965 = hide_cookie2 = False
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/elements.py 0000664 0000000 0000000 00000035565 13726205233 0025447 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from __future__ import print_function
import os
import re
import sys
from collections import OrderedDict
from copy import deepcopy
import traceback
import lxml.html
from weboob.tools.log import getLogger, DEBUG_FILTERS
from weboob.tools.compat import basestring, unicode, with_metaclass
from weboob.browser.pages import NextPage
from weboob.capabilities.base import FetchError
from .filters.standard import _Filter, CleanText
from .filters.html import AttributeNotFound, XPathNotFound
__all__ = ['DataError', 'AbstractElement', 'ListElement', 'ItemElement', 'TableElement', 'SkipItem']
def generate_table_element(doc, head_xpath, cleaner=CleanText):
"""
Prints generated base code for TableElement/TableCell usage.
It is intended for development purposes, typically in weboob-debug.
:param doc: lxml tree of the page (e.g. browser.page.doc)
:param head_xpath: xpath of header columns (e.g. //table//th)
:type head_xpath: str
:param cleaner: cleaner class (Filter)
:type cleaner: Filter
"""
from unidecode import unidecode
indent = 4
headers = doc.xpath(head_xpath)
cols = dict()
for el in headers:
th = cleaner.clean(el)
cols.update({re.sub('[^a-zA-Z]', '_', unidecode(th)).lower(): th})
print(' ' * indent + '@method')
print(' ' * indent + 'class get_items(TableElement):')
if cleaner is not CleanText:
print(' ' * indent * 2 + 'cleaner = %s' % cleaner.__name__)
print(' ' * indent * 2 + 'head_xpath = ' + repr(head_xpath))
print(' ' * indent * 2 + 'item_xpath = ' + repr('...') + '\n')
for col, name in cols.items():
print(' ' * indent * 2 + 'col_' + col + ' = ' + repr(name))
print('\n' + ' ' * indent * 2 + 'class item(ItemElement):')
print(' ' * indent * 3 + 'klass = BaseObject' + '\n')
for col in cols:
print(' ' * indent * 3 + 'obj_' + col + ' = ' + "TableCell('%s') & CleanText()" % col)
class DataError(Exception):
"""
Returned data from pages are incoherent.
"""
def method(klass):
"""
Class-decorator to call it as a method.
"""
def inner(self, *args, **kwargs):
return klass(self)(*args, **kwargs)
inner.klass = klass
return inner
class AbstractElement(object):
_creation_counter = 0
condition = None
def __init__(self, page, parent=None, el=None):
self.page = page
self.parent = parent
if el is not None:
self.el = el
elif parent is not None:
self.el = parent.el
else:
self.el = page.doc
parent_logger = None
if self.page:
parent_logger = self.page.logger
self.logger = getLogger(self.__class__.__name__.lower(), parent_logger)
self.fill_env(page, parent)
# Used by debug
self._random_id = AbstractElement._creation_counter
AbstractElement._creation_counter += 1
self.loaders = {}
def use_selector(self, func, key=None):
if isinstance(func, _Filter):
func._obj = self
func._key = key
value = func(self)
elif isinstance(func, type) and issubclass(func, ItemElement):
value = func(self.page, self, self.el)()
elif isinstance(func, type) and issubclass(func, ListElement):
value = list(func(self.page, self, self.el)())
elif callable(func):
value = func()
else:
value = deepcopy(func)
return value
def parse(self, obj):
pass
def cssselect(self, *args, **kwargs):
return self.el.cssselect(*args, **kwargs)
def xpath(self, *args, **kwargs):
return self.el.xpath(*args, **kwargs)
def handle_loaders(self):
for attrname in dir(self):
m = re.match('load_(.*)', attrname)
if not m:
continue
name = m.group(1)
if name in self.loaders:
continue
loader = getattr(self, attrname)
self.loaders[name] = self.use_selector(loader, key=attrname)
def fill_env(self, page, parent=None):
if parent is not None:
self.env = deepcopy(parent.env)
else:
self.env = deepcopy(page.params)
class ListElement(AbstractElement):
item_xpath = None
empty_xpath = None
flush_at_end = False
ignore_duplicate = False
def __init__(self, *args, **kwargs):
super(ListElement, self).__init__(*args, **kwargs)
self.objects = OrderedDict()
def __call__(self, *args, **kwargs):
for key, value in kwargs.items():
self.env[key] = value
return self.__iter__()
def find_elements(self):
"""
Get the nodes that will have to be processed.
This method can be overridden if xpath filters are not
sufficient.
"""
if self.item_xpath is not None:
element_list = self.el.xpath(self.item_xpath)
if element_list:
for el in element_list:
yield el
elif self.empty_xpath is not None and not self.el.xpath(self.empty_xpath):
# Send a warning if no item_xpath node was found and an empty_xpath is defined
self.logger.warning('No element matched the item_xpath and the defined empty_xpath was not found!')
else:
yield self.el
def __iter__(self):
if self.condition is not None and not self.condition():
return
self.parse(self.el)
items = []
for el in self.find_elements():
for attrname in dir(self):
attr = getattr(self, attrname)
if isinstance(attr, type) and issubclass(attr, AbstractElement) and attr != type(self):
item = attr(self.page, self, el)
if item.condition is not None and not item.condition():
continue
item.handle_loaders()
items.append(item)
for item in items:
for obj in item:
obj = self.store(obj)
if obj and not self.flush_at_end:
yield obj
if self.flush_at_end:
for obj in self.flush():
yield obj
self.check_next_page()
def flush(self):
for obj in self.objects.values():
yield obj
def check_next_page(self):
if not hasattr(self, 'next_page'):
return
next_page = getattr(self, 'next_page')
try:
value = self.use_selector(next_page)
except (AttributeNotFound, XPathNotFound):
return
if value is None:
return
raise NextPage(value)
def store(self, obj):
if obj.id:
if obj.id in self.objects:
if self.ignore_duplicate:
self.logger.warning('There are two objects with the same ID! %s' % obj.id)
return
else:
raise DataError('There are two objects with the same ID! %s' % obj.id)
self.objects[obj.id] = obj
return obj
class SkipItem(Exception):
"""
Raise this exception in an :class:`ItemElement` subclass to skip an item.
"""
class _ItemElementMeta(type):
"""
Private meta-class used to keep order of obj_* attributes in :class:`ItemElement`.
"""
def __new__(mcs, name, bases, attrs):
_attrs = []
for base in bases:
if hasattr(base, '_attrs'):
_attrs += base._attrs
filters = [(re.sub('^obj_', '', attr_name), attrs[attr_name]) for attr_name, obj in attrs.items() if attr_name.startswith('obj_')]
# constants first, then filters, then methods
filters.sort(key=lambda x: x[1]._creation_counter if hasattr(x[1], '_creation_counter') else (sys.maxsize if callable(x[1]) else 0))
attrs['_class_file'], attrs['_class_line'] = traceback.extract_stack()[-2][:2]
new_class = super(_ItemElementMeta, mcs).__new__(mcs, name, bases, attrs)
new_class._attrs = _attrs + [f[0] for f in filters]
return new_class
class ItemElement(with_metaclass(_ItemElementMeta, AbstractElement)):
_attrs = None
_loaders = None
klass = None
validate = None
skip_optional_fields_errors = False
class Index(object):
pass
def __init__(self, *args, **kwargs):
super(ItemElement, self).__init__(*args, **kwargs)
self.obj = None
self.saved_attrib = {} # safer way would be to clone lxml tree
def build_object(self):
if self.klass is None:
return
return self.klass()
def _restore_attrib(self):
for el in self.saved_attrib:
el.attrib.clear()
el.attrib.update(self.saved_attrib[el])
self.saved_attrib = {}
def should_highlight(self):
try:
responses_dirname = self.page.browser.responses_dirname and self.page.browser.highlight_el
if not responses_dirname:
return False
if not self.el.getroottree():
return False
except AttributeError:
return False
else:
return True
def _write_highlighted(self):
if not self.should_highlight():
return
responses_dirname = self.page.browser.responses_dirname
html = lxml.html.tostring(self.el.getroottree().getroot())
fn = os.path.join(responses_dirname, 'obj-%s.html' % self._random_id)
with open(fn, 'w') as fd:
fd.write(html)
self.logger.debug('highlighted object to %s', fn)
def __call__(self, obj=None):
if obj is not None:
self.obj = obj
for obj in self:
return obj
def __iter__(self):
if self.condition is not None and not self.condition():
return
highlight = False
try:
if self.should_highlight():
self.saved_attrib[self.el] = dict(self.el.attrib)
self.el.attrib['style'] = 'color: white !important; background: orange !important;'
try:
if self.obj is None:
self.obj = self.build_object()
self.parse(self.el)
self.handle_loaders()
for attr in self._attrs:
self.handle_attr(attr, getattr(self, 'obj_%s' % attr))
except SkipItem:
return
if self.validate is not None and not self.validate(self.obj):
return
highlight = True
finally:
if highlight:
self._write_highlighted()
self._restore_attrib()
yield self.obj
def handle_attr(self, key, func):
try:
value = self.use_selector(func, key=key)
except SkipItem as e:
# Help debugging as tracebacks do not give us the key
self.logger.debug("Attribute %s raises a %r", key, e)
raise
except Exception as e:
# If we are here, we have probably a real parsing issue
self.logger.warning('Attribute %s (in %s:%s) raises %s', key, self._class_file, self._class_line, repr(e))
if not self.skip_optional_fields_errors or key not in self.obj._fields or self.obj._fields[key].mandatory:
raise
else:
value = FetchError
logger = getLogger('b2filters')
logger.log(DEBUG_FILTERS, "%s.%s = %r" % (self._random_id, key, value))
setattr(self.obj, key, value)
class TableElement(ListElement):
head_xpath = None
cleaner = CleanText
def __init__(self, *args, **kwargs):
super(TableElement, self).__init__(*args, **kwargs)
self._cols = {}
columns = {}
for attrname in dir(self):
m = re.match('col_(.*)', attrname)
if m:
cols = getattr(self, attrname)
if not isinstance(cols, (list,tuple)):
cols = [cols]
columns[m.group(1)] = [s.lower() if isinstance(s, (str, unicode)) else s for s in cols]
colnum = 0
for el in self.el.xpath(self.head_xpath):
title = self.cleaner.clean(el)
for name, titles in columns.items():
if name in self._cols:
continue
if title.lower() in [s for s in titles if isinstance(s, (str, unicode))] or \
any(map(lambda x: x.match(title), [s for s in titles if isinstance(s, type(re.compile('')))])):
self._cols[name] = colnum
try:
colnum += int(el.attrib.get('colspan', 1))
except (ValueError, AttributeError):
colnum += 1
def get_colnum(self, name):
return self._cols.get(name, None)
class DictElement(ListElement):
def find_elements(self):
if self.item_xpath is None:
selector = []
elif isinstance(self.item_xpath, basestring):
selector = self.item_xpath.split('/')
else:
selector = self.item_xpath
bases = [self.el]
for key in selector:
if key == '*':
bases = sum([el if isinstance(el, list) else list(el.values()) for el in bases], [])
else:
bases = [el[int(key)] if isinstance(el, list) else el[key] for el in bases]
for base in bases:
for el in base:
yield el
def magic_highlight(els, open_browser=True):
"""Open a web browser with the document open and the element highlighted"""
import lxml.html
import webbrowser
import tempfile
if not els:
raise Exception('no elements to highlight')
if not isinstance(els, (list, tuple)):
els = [els]
saved = {}
for el in els:
saved[el] = el.attrib.get('style', '')
el.attrib['style'] = 'color: white !important; background: red !important;'
html = lxml.html.tostring(el.xpath('/*')[0])
for el in els:
el.attrib['style'] = saved[el]
_, fn = tempfile.mkstemp(dir='/tmp', prefix='weboob-highlight', suffix='.html')
with open(fn, 'w') as fd:
fd.write(html)
print('Saved to %r' % fn)
if open_browser:
webbrowser.open('file://%s' % fn)
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/exceptions.py 0000664 0000000 0000000 00000004035 13726205233 0026000 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
import datetime
from dateutil.relativedelta import relativedelta
from requests.exceptions import HTTPError
from weboob.exceptions import (
BrowserHTTPError, BrowserHTTPNotFound, BrowserUnavailable,
)
class HTTPNotFound(HTTPError, BrowserHTTPNotFound):
pass
class ClientError(HTTPError, BrowserHTTPError):
pass
class ServerError(HTTPError, BrowserHTTPError):
pass
class LoggedOut(Exception):
pass
class BrowserTooManyRequests(BrowserUnavailable):
"""
Client tries to perform too many requests within a certain timeframe.
The module should set the next_try if possible, else it is set to 24h.
"""
def __init__(self, message='', next_try=None):
super(BrowserTooManyRequests, self).__init__(message)
if isinstance(next_try, datetime.date) and not isinstance(next_try, datetime.datetime):
next_try = datetime.datetime.combine(next_try, datetime.datetime.min.time())
if next_try is None:
next_try = datetime.datetime.now() + relativedelta(days=1)
if not isinstance(next_try, datetime.datetime):
raise TypeError('next_try value should be a datetime.')
self.next_try = next_try
def __str__(self):
return super(BrowserTooManyRequests, self).__str__() or 'Too many requests, next_try set %s' % self.next_try
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/filters/ 0000775 0000000 0000000 00000000000 13726205233 0024713 5 ustar 00root root 0000000 0000000 woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/filters/__init__.py 0000664 0000000 0000000 00000001331 13726205233 0027022 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/filters/base.py 0000664 0000000 0000000 00000013626 13726205233 0026207 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 2017 weboob project
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
from functools import wraps
import lxml.html
from weboob.exceptions import ParseError
from weboob.tools.compat import unicode, basestring
from weboob.tools.log import getLogger, DEBUG_FILTERS
__all__ = ['FilterError', 'ItemNotFound', 'Filter',]
class NoDefault(object):
def __repr__(self):
return 'NO_DEFAULT'
_NO_DEFAULT = NoDefault()
class FilterError(ParseError):
pass
class ItemNotFound(FilterError):
pass
class _Filter(object):
_creation_counter = 0
def __init__(self, default=_NO_DEFAULT):
self._key = None
self._obj = None
self.default = default
self._creation_counter = _Filter._creation_counter
_Filter._creation_counter += 1
def __or__(self, o):
self.default = o
return self
def __and__(self, o):
if isinstance(o, type) and issubclass(o, _Filter):
o = o()
o.selector = self
return o
def default_or_raise(self, exception):
if self.default is not _NO_DEFAULT:
return self.default
else:
raise exception
def __str__(self):
return self.__class__.__name__
def highlight_el(self, el, item=None):
obj = self._obj or item
try:
if not hasattr(obj, 'saved_attrib'):
return
if not obj.page.browser.highlight_el:
return
except AttributeError:
return
if el not in obj.saved_attrib:
obj.saved_attrib[el] = dict(el.attrib)
el.attrib['style'] = 'color: white !important; background: red !important;'
if self._key:
el.attrib['title'] = 'weboob field: %s' % self._key
def debug(*args):
"""
A decorator function to provide some debug information
in Filters.
It prints by default the name of the Filter and the input value.
"""
def wraper(function):
@wraps(function)
def print_debug(self, value):
logger = getLogger('b2filters')
result = ''
outputvalue = value
if isinstance(value, list):
from lxml import etree
outputvalue = ''
first = True
for element in value:
if first:
first = False
else:
outputvalue += ', '
if isinstance(element, etree.ElementBase):
outputvalue += "%s" % etree.tostring(element, encoding=unicode)
else:
outputvalue += "%r" % element
if self._obj is not None:
result += "%s" % self._obj._random_id
if self._key is not None:
result += ".%s" % self._key
name = str(self)
result += " %s(%r" % (name, outputvalue)
for arg in self.__dict__:
if arg.startswith('_') or arg == u"selector":
continue
if arg == u'default' and getattr(self, arg) == _NO_DEFAULT:
continue
result += ", %s=%r" % (arg, getattr(self, arg))
result += u')'
logger.log(DEBUG_FILTERS, result)
res = function(self, value)
return res
return print_debug
return wraper
class Filter(_Filter):
"""
Class used to filter on a HTML element given as call parameter to return
matching elements.
Filters can be chained, so the parameter supplied to constructor can be
either a xpath selector string, or an other filter called before.
>>> from lxml.html import etree
>>> f = CleanDecimal(CleanText('//p'), replace_dots=True)
>>> f(etree.fromstring('
blah: 229,90
'))
Decimal('229.90')
"""
def __init__(self, selector=None, default=_NO_DEFAULT):
"""
:param default: default value in case the filter fails to find or parse
the requested value
"""
super(Filter, self).__init__(default=default)
self.selector = selector
def select(self, selector, item):
if isinstance(selector, basestring):
ret = item.xpath(selector)
elif isinstance(selector, _Filter):
selector._key = self._key
selector._obj = self._obj
ret = selector(item)
elif callable(selector):
ret = selector(item)
else:
ret = selector
if isinstance(ret, lxml.html.HtmlElement):
self.highlight_el(ret, item)
elif isinstance(ret, list):
for el in ret:
if isinstance(el, lxml.html.HtmlElement):
self.highlight_el(el, item)
return ret
def __call__(self, item):
return self.filter(self.select(self.selector, item))
@debug()
def filter(self, value):
"""
This method has to be overridden by children classes.
"""
raise NotImplementedError()
class _Selector(Filter):
def filter(self, elements):
if elements is not None:
return elements
else:
return self.default_or_raise(FilterError('Element %r not found' % self.selector))
woob-8259210387867d750dd4cc420c7b063d313198f2-weboob/weboob/browser/filters/html.py 0000664 0000000 0000000 00000024057 13726205233 0026241 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# Copyright(C) 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 Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with weboob. If not, see .
import datetime
from decimal import Decimal
import lxml.html as html
from six.moves.html_parser import HTMLParser
from weboob.tools.compat import basestring, unicode, urljoin
from weboob.tools.html import html2text
from .base import (
_NO_DEFAULT, Filter, FilterError, _Selector, debug, ItemNotFound,
_Filter,
)
from .standard import CleanText
__all__ = ['CSS', 'XPath', 'XPathNotFound', 'AttributeNotFound',
'Attr', 'Link', 'AbsoluteLink',
'CleanHTML', 'FormValue', 'HasElement',
'TableCell', 'ColumnNotFound',
'ReplaceEntities',
]
class XPathNotFound(ItemNotFound):
pass
class AttributeNotFound(ItemNotFound):
pass
class ColumnNotFound(FilterError):
pass
class CSS(_Selector):
"""Select HTML elements with a CSS selector
For example::
obj_foo = CleanText(CSS('div.main'))
will take the text of all ``