change way to describe fields of CapBaseObject, and lot of documentation

This commit is contained in:
Romain Bignon 2012-03-25 22:29:18 +02:00
commit c6a141595c
35 changed files with 1630 additions and 638 deletions

View file

@ -1,5 +0,0 @@
Applications
============
boobank
-------

View file

@ -1,5 +0,0 @@
Backends
========
cragr
-----

View file

@ -10,8 +10,7 @@ def genapi():
os.chdir('api') os.chdir('api')
for root, dirs, files in os.walk('../../../weboob/'): for root, dirs, files in os.walk('../../../weboob/'):
root = root.split('/', 4)[-1] root = root.split('/', 4)[-1]
if root.startswith('applications') or \ if root.startswith('applications'):
root.startswith('backends'):
continue continue
if root.strip(): if root.strip():
@ -26,7 +25,7 @@ def genapi():
continue continue
f, ext = f.rsplit('.', 1) f, ext = f.rsplit('.', 1)
if ext == 'pyc' or f == '__init__': if ext != 'py' or f == '__init__':
continue continue
subs.add(f) subs.add(f)

View file

@ -3,18 +3,12 @@ Weboob
This is the developer documentation. This is the developer documentation.
.. warning::
This documentation is being written.
Contents: Contents:
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
overview overview
install
backends
applications
guides/index guides/index
api/index api/index

View file

@ -1,30 +0,0 @@
Installation
============
If you install Weboob from sources, you'll want to install the Python egg in development mode.
You'll be able to update the git repository with remote changes, without re-installing the software.
And if you plan to hack on Weboob, you'll see your own changes apply the same way.
Install
-------
As root:
``# ./setup.py develop``
The development mode installation doesn't copies files, but creates an egg-link
in the Python system packages directory:
* ``/usr/lib/python2.5/site-packages`` for Python 2.5
* ``/usr/local/lib/python2.6/dist-packages`` for Python 2.6
Scripts are copied to:
* ``/usr/bin`` for Python 2.5
* ``/usr/local/bin`` for Python 2.6
Uninstall
---------
* remove the ``/usr/local/lib/python2.6/dist-packages/weboob.egg-link``
* remove the weboob line from ``/usr/local/lib/python2.6/dist-packages/easy-install.pth``

View file

@ -3,21 +3,13 @@ Overview
Weboob (Web Out Of Browsers) provides: Weboob (Web Out Of Browsers) provides:
* :doc:`applications` to interact with websites * :doc:`applications <api/tools/application/index>` to interact with websites
* :doc:`backends`, each one handles a specific website * :doc:`backends <api/tools/backend>`, each one handles a specific website
* a :doc:`core library <api/core/index>` providing all the features needed by backends * a :doc:`core library <api/core/index>` providing all the features needed by backends
* :doc:`tools <api/tools/index>` to help develop backends and applications * :doc:`tools <api/tools/index>` to help develop backends and applications
Weboob is written in Python and is distributed under the AGPLv3+ license. Weboob is written in Python and is distributed under the AGPLv3+ license.
Why using Weboob?
-----------------
* you get essential information from websites faster
* you can write scripts using weboob to automate tasks
* you can extend websites features
* it helps blind people using crappy websites
Architecture Architecture
------------ ------------

View file

@ -18,23 +18,32 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, StringField, Field
__all__ = ['ICapAccount'] __all__ = ['AccountRegisterError', 'Account', 'StatusField', 'ICapAccount']
class AccountRegisterError(Exception): class AccountRegisterError(Exception):
pass """
Raised when there is an error during registration.
"""
class Account(CapBaseObject): class Account(CapBaseObject):
"""
Describe an account and its properties.
"""
login = StringField('Login')
password = StringField('Password')
properties = Field('List of key/value properties', dict)
def __init__(self, id=None): def __init__(self, id=None):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('login', basestring)
self.add_field('password', basestring)
self.add_field('properties', dict)
class StatusField(object): class StatusField(object):
"""
Field of an account status.
"""
FIELD_TEXT = 0x001 # the value is a long text FIELD_TEXT = 0x001 # the value is a long text
FIELD_HTML = 0x002 # the value is HTML formated FIELD_HTML = 0x002 # the value is HTML formated
@ -46,8 +55,14 @@ class StatusField(object):
class ICapAccount(IBaseCap): class ICapAccount(IBaseCap):
# This class constant may be a list of Value* objects. If the value remains """
# None, weboob considers that register_account() isn't supported. Capability for websites when you can create and manage accounts.
:var ACCOUNT_REGISTER_PROPERTIES: This class constant may be a list of
:class:`weboob.tools.value.Value` objects.
If the value remains None, weboob considers
that :func:`register_account` isn't supported.
"""
ACCOUNT_REGISTER_PROPERTIES = None ACCOUNT_REGISTER_PROPERTIES = None
@staticmethod @staticmethod
@ -58,7 +73,9 @@ class ICapAccount(IBaseCap):
This is a static method, it would be called even if the backend is This is a static method, it would be called even if the backend is
instancied. instancied.
@param account an Account object which describe the account to create :param account: describe the account to create
:type account: :class:`Account`
:raises: :class:`AccountRegisterError`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -84,6 +101,6 @@ class ICapAccount(IBaseCap):
""" """
Get status of the current account. Get status of the current account.
@return a list of fields :returns: a list of fields
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,30 +18,45 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, date from datetime import date, datetime
from .base import CapBaseObject from .base import CapBaseObject, Field, StringField, DateField, FloatField, IntField
from .collection import ICapCollection from .collection import ICapCollection
__all__ = ['Account', 'AccountNotFound', 'TransferError', 'ICapBank', 'Transaction'] __all__ = ['AccountNotFound', 'TransferError', 'Recipient', 'Account', 'Transaction', 'Transfer', 'ICapBank']
class AccountNotFound(Exception): class AccountNotFound(Exception):
def __init__(self, msg=None): """
if msg is None: Raised when an account is not found.
msg = 'Account not found' """
def __init__(self, msg='Account not found'):
Exception.__init__(self, msg) Exception.__init__(self, msg)
class TransferError(Exception): class TransferError(Exception):
pass """
A transfer has failed.
"""
class Recipient(CapBaseObject): class Recipient(CapBaseObject):
"""
Recipient of a transfer.
"""
label = StringField('Name')
def __init__(self): def __init__(self):
CapBaseObject.__init__(self, 0) CapBaseObject.__init__(self, 0)
self.add_field('label', basestring)
class Account(Recipient): class Account(Recipient):
"""
Bank account.
It is a child class of :class:`Recipient`, because an account can be
a recipient of a transfer.
"""
TYPE_UNKNOWN = 0 TYPE_UNKNOWN = 0
TYPE_CHECKING = 1 # Transaction, everyday transactions TYPE_CHECKING = 1 # Transaction, everyday transactions
TYPE_SAVINGS = 2 # Savings/Deposit, can be used for everyday banking TYPE_SAVINGS = 2 # Savings/Deposit, can be used for everyday banking
@ -50,17 +65,18 @@ class Account(Recipient):
TYPE_MARKET = 5 # Stock market or other variable investments TYPE_MARKET = 5 # Stock market or other variable investments
TYPE_JOINT = 6 # Joint account TYPE_JOINT = 6 # Joint account
def __init__(self): type = IntField('Type of account', default=TYPE_UNKNOWN)
Recipient.__init__(self) balance = FloatField('Balance on this bank account')
self.add_field('type', int, self.TYPE_UNKNOWN) coming = FloatField('Coming balance')
self.add_field('balance', float)
self.add_field('coming', float)
def __repr__(self): def __repr__(self):
return u"<Account id=%r label=%r>" % (self.id, self.label) return u"<Account id=%r label=%r>" % (self.id, self.label)
class Transaction(CapBaseObject): class Transaction(CapBaseObject):
"""
Bank transaction.
"""
TYPE_UNKNOWN = 0 TYPE_UNKNOWN = 0
TYPE_TRANSFER = 1 TYPE_TRANSFER = 1
TYPE_ORDER = 2 TYPE_ORDER = 2
@ -72,47 +88,77 @@ class Transaction(CapBaseObject):
TYPE_LOAN_PAYMENT = 8 TYPE_LOAN_PAYMENT = 8
TYPE_BANK = 9 TYPE_BANK = 9
def __init__(self, id): date = DateField('Debit date')
CapBaseObject.__init__(self, id) rdate = DateField('Real date, when the payment has been made')
self.add_field('date', (basestring, datetime, date)) # debit date type = IntField('Type of transaction, use TYPE_* constants', default=TYPE_UNKNOWN)
self.add_field('rdate', (datetime, date)) # real date, when the payment has been made raw = StringField('Raw label of the transaction')
self.add_field('type', int, self.TYPE_UNKNOWN) category = StringField('Category of transaction')
self.add_field('raw', unicode) label = StringField('Pretty label')
self.add_field('category', unicode) amount = FloatField('Amount of transaction')
self.add_field('label', unicode)
self.add_field('amount', float)
def __repr__(self): def __repr__(self):
return "<Transaction date='%s' label='%s' amount=%s>" % (self.date, return "<Transaction date='%s' label='%s' amount=%s>" % (self.date,
self.label, self.amount) self.label, self.amount)
class Transfer(CapBaseObject): class Transfer(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Transfer from an account to a recipient.
self.add_field('amount', float) """
self.add_field('date', (basestring, datetime, date))
self.add_field('origin', (int, long, basestring)) amount = FloatField('Amount to transfer')
self.add_field('recipient', (int, long, basestring)) date = Field('Date of transfer', basestring, date, datetime)
origin = Field('Origin of transfer', int, long, basestring)
recipient = Field('Recipient', int, long, basestring)
class ICapBank(ICapCollection): class ICapBank(ICapCollection):
"""
Capability of bank websites to see accounts and transactions.
"""
def iter_resources(self, objs, split_path): def iter_resources(self, objs, split_path):
"""
Iter resources.
Default implementation of this method is to return on top-level
all accounts (by calling :func:`iter_accounts`).
:param objs: type of objects to get
:type objs: tuple[:class:`CapBaseObject`]
:param split_path: path to discover
:type split_path: :class:`list`
:rtype: iter[:class:`BaseCapObject`]
"""
if Account in objs: if Account in objs:
self._restrict_level(split_path) self._restrict_level(split_path)
return self.iter_accounts() return self.iter_accounts()
def iter_accounts(self): def iter_accounts(self):
"""
Iter accounts.
:rtype: iter[:class:`Account`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_account(self, _id): def get_account(self, id):
"""
Get an account from its ID.
:param id: ID of the account
:type id: :class:`str`
:rtype: :class:`Account`
:raises: :class:`AccountNotFound`
"""
raise NotImplementedError() raise NotImplementedError()
def iter_history(self, account): def iter_history(self, account):
""" """
Iter history of transactions on a specific account. Iter history of transactions on a specific account.
@param account [Account] :param account: account to get history
@return [iter(Transaction)] :type account: :class:`Account`
:rtype: iter[:class:`Transaction`]
:raises: :class:`AccountNotFound`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -120,8 +166,10 @@ class ICapBank(ICapCollection):
""" """
Iter coming transactions on a specific account. Iter coming transactions on a specific account.
@param account [Account] :param account: account to get coming transactions
@return [iter(Transaction)] :type account: :class:`Account`
:rtype: iter[:class:`Transaction`]
:raises: :class:`AccountNotFound`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -129,8 +177,10 @@ class ICapBank(ICapCollection):
""" """
Iter recipients availables for a transfer from a specific account. Iter recipients availables for a transfer from a specific account.
@param account [Account] account which initiate the transfer :param account: account which initiate the transfer
@return [iter(Recipient)] :type account: :class:`Account`
:rtype: iter[:class:`Recipient`]
:raises: :class:`AccountNotFound`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -138,10 +188,15 @@ class ICapBank(ICapCollection):
""" """
Make a transfer from an account to a recipient. Make a transfer from an account to a recipient.
@param account [Account] account to take money :param account: account to take money
@param recipient [Recipient] account to send money :type account: :class:`Account`
@param amount [float] amount :param recipient: account to send money
@param reason [str] reason of transfer :type recipient: :class:`Recipient`
@return [Transfer] a Transfer object :param amount: amount
:type amount: :class:`float`
:param reason: reason of transfer
:type reason: :class:`unicode`
:rtype: :class:`Transfer`
:raises: :class:`AccountNotFound`, :class:`TransferError`
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,14 +18,28 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from weboob.tools.misc import iter_fields import datetime
from dateutil.parser import parse as parse_dt
from copy import deepcopy
from weboob.tools.misc import to_unicode
from weboob.tools.ordereddict import OrderedDict
__all__ = ['FieldNotFound', 'IBaseCap', 'NotAvailable', 'NotLoaded', __all__ = ['FieldNotFound', 'NotAvailable', 'NotLoaded', 'IBaseCap',
'CapBaseObject'] 'Field', 'IntField', 'FloatField', 'StringField', 'BytesField',
'DateField', 'DeltaField', 'CapBaseObject']
class FieldNotFound(Exception): class FieldNotFound(Exception):
"""
A field isn't found.
:param obj: object
:type obj: :class:`CapBaseObject`
:param field: field not found
:type field: :class:`Field`
"""
def __init__(self, obj, field): def __init__(self, obj, field):
Exception.__init__(self, Exception.__init__(self,
u'Field "%s" not found for object %s' % (field, obj)) u'Field "%s" not found for object %s' % (field, obj))
@ -43,6 +57,9 @@ class NotAvailableMeta(type):
class NotAvailable(object): class NotAvailable(object):
"""
Constant to use on non available fields.
"""
__metaclass__ = NotAvailableMeta __metaclass__ = NotAvailableMeta
@ -58,41 +75,186 @@ class NotLoadedMeta(type):
class NotLoaded(object): class NotLoaded(object):
"""
Constant to use on not loaded fields.
When you use :func:`weboob.tools.backend.BaseBackend.fillobj` on a object based on :class:`CapBaseObject`,
it will request all fields with this value.
"""
__metaclass__ = NotLoadedMeta __metaclass__ = NotLoadedMeta
class IBaseCap(object): class IBaseCap(object):
pass """
This is the base class for all capabilities.
A capability may define abstract methods (which raise :class:`NotImplementedError`)
with an explicit docstring to tell backends how to implement them.
Also, it may define some *objects*, using :class:`CapBaseObject`.
"""
class Field(object):
"""
Field of a :class:`CapBaseObject` class.
:param doc: docstring of the field
:type doc: :class:`str`
:param args: list of types accepted
:param default: default value of this field. If not specified, :class:`NotLoaded` is used.
"""
_creation_counter = 0
def __init__(self, doc, *args, **kwargs):
self.types = ()
self.value = kwargs.get('default', NotLoaded)
self.doc = doc
for arg in args:
if isinstance(arg, type):
self.types += (arg,)
else:
raise TypeError('Arguments must be types')
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
def convert(self, value):
"""
Convert value to the wanted one.
"""
return value
class IntField(Field):
"""
A field which accepts only :class:`int` and :class:`long` types.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, int, long, **kwargs)
def convert(self, value):
return int(value)
class FloatField(Field):
"""
A field which accepts only :class:`float` type.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, float, **kwargs)
def convert(self, value):
return float(value)
class StringField(Field):
"""
A field which accepts only :class:`unicode` strings.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, unicode, **kwargs)
def convert(self, value):
return to_unicode(value)
class BytesField(Field):
"""
A field which accepts only :class:`str` strings.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, str, **kwargs)
def convert(self, value):
if isinstance(value, unicode):
value = value.encode('utf-8')
return str(value)
class DateField(Field):
"""
A field which accepts only :class:`datetime.date` and :class:`datetime.datetime` types.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, datetime.date, datetime.datetime, **kwargs)
def convert(self, value):
if isinstance(value, basestring):
return parse_dt(value)
return value
class TimeField(Field):
"""
A field which accepts only :class:`datetime.time` and :class:`datetime.time` types.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, datetime.time, datetime.datetime, **kwargs)
class DeltaField(Field):
"""
A field which accepts only :class:`datetime.timedelta` type.
"""
def __init__(self, doc, **kwargs):
Field.__init__(self, doc, datetime.timedelta, **kwargs)
class _CapBaseObjectMeta(type):
def __new__(cls, name, bases, attrs):
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1]._creation_counter)
new_class = super(_CapBaseObjectMeta, cls).__new__(cls, name, bases, attrs)
if new_class._fields is None:
new_class._fields = OrderedDict()
else:
new_class._fields = deepcopy(new_class._fields)
new_class._fields.update(fields)
assert new_class.__doc__ is not None
if new_class.__doc__ is None:
new_class.__doc__ = ''
for name, field in fields:
doc = '(%s) %s' % (', '.join([':class:`%s`' % v.__name__ for v in field.types]), field.doc)
if field.value is not NotLoaded:
doc += ' (default: %s)' % field.value
new_class.__doc__ += '\n:var %s: %s' % (name, doc)
return new_class
class CapBaseObject(object): class CapBaseObject(object):
FIELDS = None """
_attribs = None This is the base class for a capability object.
A capability interface may specify to return several kind of objects, to formalise
retrieved information from websites.
As python is a flexible language where variables are not typed, we use a system to
force backends to set wanted values on all fields. To do that, we use the :class:`Field`
class and all derived ones.
For example::
class Transfer(CapBaseObject):
" Transfer from an account to a recipient. "
amount = FloatField('Amount to transfer')
date = Field('Date of transfer', basestring, date, datetime)
origin = Field('Origin of transfer', int, long, basestring)
recipient = Field('Recipient', int, long, basestring)
The docstring is mandatory.
"""
__metaclass__ = _CapBaseObjectMeta
_fields = None
def __init__(self, id, backend=None): def __init__(self, id, backend=None):
self.id = id self.id = to_unicode(id)
self.backend = backend self.backend = backend
self._fields = deepcopy(self._fields)
@property @property
def fullid(self): def fullid(self):
"""
Full ID of the object, in form '**ID@backend**'.
"""
return '%s@%s' % (self.id, self.backend) return '%s@%s' % (self.id, self.backend)
def add_field(self, name, type, value=NotLoaded):
"""
Add a field in list, which needs to be of type @type.
@param name [str] name of field
@param type [class] type accepted (can be a tuple of types)
@param value [object] value set to attribute (default is NotLoaded)
"""
if not isinstance(self.FIELDS, list):
self.FIELDS = []
self.FIELDS.append(name)
if self._attribs is None:
self._attribs = {}
self._attribs[name] = self._AttribValue(type, value)
def __iscomplete__(self): def __iscomplete__(self):
""" """
Return True if the object is completed. Return True if the object is completed.
@ -111,6 +273,9 @@ class CapBaseObject(object):
def set_empty_fields(self, value, excepts=()): def set_empty_fields(self, value, excepts=()):
""" """
Set the same value on all empty fields. Set the same value on all empty fields.
:param value: value to set on all empty fields
:param excepts: if specified, do not change fields listed
""" """
for key, old_value in self.iter_fields(): for key, old_value in self.iter_fields():
if old_value in (None, NotLoaded, NotAvailable) and \ if old_value in (None, NotLoaded, NotAvailable) and \
@ -119,22 +284,16 @@ class CapBaseObject(object):
def iter_fields(self): def iter_fields(self):
""" """
Iterate on the FIELDS keys and values. Iterate on the fields keys and values.
Can be overloaded to iterate on other things. Can be overloaded to iterate on other things.
@return [iter(key,value)] iterator on key, value :rtype: iter[(key, value)]
""" """
if self.FIELDS is None: yield 'id', self.id
yield 'id', self.id for name, field in self._fields.iteritems():
for key, value in iter_fields(self): yield name, field.value
if key not in ('id', 'backend', 'FIELDS'):
yield key, value
else:
yield 'id', self.id
for attrstr in self.FIELDS:
yield attrstr, getattr(self, attrstr)
def __eq__(self, obj): def __eq__(self, obj):
if isinstance(obj, CapBaseObject): if isinstance(obj, CapBaseObject):
@ -142,29 +301,32 @@ class CapBaseObject(object):
else: else:
return False return False
class _AttribValue(object):
def __init__(self, type, value):
self.type = type
self.value = value
def __getattr__(self, name): def __getattr__(self, name):
if self._attribs is not None and name in self._attribs: if self._fields is not None and name in self._fields:
return self._attribs[name].value return self._fields[name].value
else: else:
raise AttributeError, "'%s' object has no attribute '%s'" % ( raise AttributeError, "'%s' object has no attribute '%s'" % (
self.__class__.__name__, name) self.__class__.__name__, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
try: try:
attr = (self._attribs or {})[name] attr = (self._fields or {})[name]
except KeyError: except KeyError:
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
else: else:
if not isinstance(value, attr.type) and \ try:
# Try to convert value to the wanted one.
value = attr.convert(value)
except Exception:
# error during conversion, it will probably not
# match the wanted following types, so we'll
# raise ValueError.
pass
if not isinstance(value, attr.types) and \
value is not NotLoaded and \ value is not NotLoaded and \
value is not NotAvailable and \ value is not NotAvailable and \
value is not None: value is not None:
raise ValueError( raise ValueError(
'Value for "%s" needs to be of type %r, not %r' % ( 'Value for "%s" needs to be of type %r, not %r' % (
name, attr.type, type(value))) name, attr.types, type(value)))
attr.value = value attr.value = value

View file

@ -17,74 +17,133 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, date from .base import CapBaseObject, StringField, DateField, FloatField
from .base import CapBaseObject
from .collection import ICapCollection from .collection import ICapCollection
__all__ = ['Subscription', 'SubscriptionNotFound', 'ICapBill', 'Detail'] __all__ = ['SubscriptionNotFound', 'BillNotFound', 'Detail', 'Bill', 'Subscription', 'ICapBill']
class SubscriptionNotFound(Exception): class SubscriptionNotFound(Exception):
def __init__(self, msg=None): """
if msg is None: Raised when a subscription is not found.
msg = 'Subscription not found' """
def __init__(self, msg='Subscription not found'):
Exception.__init__(self, msg) Exception.__init__(self, msg)
class BillNotFound(Exception): class BillNotFound(Exception):
def __init__(self, msg=None): """
if msg is None: Raised when a bill is not found.
msg = 'Bill not found' """
def __init__(self, msg='Bill not found'):
Exception.__init__(self, msg) Exception.__init__(self, msg)
class Detail(CapBaseObject): class Detail(CapBaseObject):
"""
Detail of a subscription
"""
label = StringField('label of the detail line')
infos = StringField('information')
datetime = DateField('date information')
price = FloatField('price')
def __init__(self): def __init__(self):
CapBaseObject.__init__(self, 0) CapBaseObject.__init__(self, 0)
self.add_field('label', basestring)
self.add_field('infos', basestring)
self.add_field('datetime', datetime)
self.add_field('price', float)
class Bill(CapBaseObject): class Bill(CapBaseObject):
"""
Bill.
"""
date = DateField('date of the bill')
format = StringField('format of the bill')
label = StringField('label of bill')
idparent = StringField('id of the parent subscription')
def __init__(self): def __init__(self):
CapBaseObject.__init__(self, 0) CapBaseObject.__init__(self, 0)
self.add_field('date', date)
self.add_field('format', basestring)
self.add_field('label', basestring)
self.add_field('idparent', basestring)
class Subscription(CapBaseObject): class Subscription(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Subscription to a service.
self.add_field('label', basestring) """
self.add_field('subscriber', basestring) label = StringField('label of subscription')
subscriber = StringField('whe has subscribed')
class ICapBill(ICapCollection): class ICapBill(ICapCollection):
def iter_resources(self, objs, split_path): def iter_resources(self, objs, split_path):
"""
Iter resources. Will return :func:`iter_subscriptions`.
"""
if Subscription in objs: if Subscription in objs:
self._restrict_level(split_path) self._restrict_level(split_path)
return self.iter_subscription() return self.iter_subscription()
def iter_subscription(self): def iter_subscription(self):
"""
Iter subscriptions.
:rtype: iter[:class:`Subscription`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_subscription(self, _id): def get_subscription(self, _id):
"""
Get a subscription.
:param _id: ID of subscription
:rtype: :class:`Subscription`
:raises: :class:`SubscriptionNotFound`
"""
raise NotImplementedError() raise NotImplementedError()
def iter_history(self, subscription): def iter_history(self, subscription):
"""
Iter history of a subscription.
:param subscription: subscription to get history
:type subscription: :class:`Subscription`
:rtype: iter[:class:`Detail`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_bill(self, id): def get_bill(self, id):
"""
Get a bill.
:param id: ID of bill
:rtype: :class:`Bill`
:raises: :class:`BillNotFound`
"""
raise NotImplementedError() raise NotImplementedError()
def download_bill(self, id): def download_bill(self, id):
"""
Download a bill.
:param id: ID of bill
:rtype: str
:raises: :class:`BillNotFound`
"""
raise NotImplementedError() raise NotImplementedError()
def iter_bills(self, subscription): def iter_bills(self, subscription):
"""
Iter bills.
:param subscription: subscription to get bills
:type subscription: :class:`Subscription`
:rtype: iter[:class:`Bill`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_details(self, subscription): def get_details(self, subscription):
"""
Get details of a subscription.
:param subscription: subscription to get bills
:type subscription: :class:`Subscription`
:rtype: iter[:class:`Detail`]
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -17,30 +17,49 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, timedelta
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, Field, StringField, DateField, \
IntField, DeltaField
__all__ = ['ICapBugTracker'] __all__ = ['IssueError', 'Project', 'User', 'Version', 'Status', 'Attachment',
'Change', 'Update', 'Issue', 'Query', 'ICapBugTracker']
class IssueError(Exception): class IssueError(Exception):
pass """
Raised when there is an error with an issue.
"""
class Project(CapBaseObject): class Project(CapBaseObject):
"""
Represents a project.
"""
name = StringField('Name of the project')
members = Field('Members of projects', list)
versions = Field('List of versions available for this project', list)
categories = Field('All categories', list)
statuses = Field('Available statuses for issues', list)
def __init__(self, id, name): def __init__(self, id, name):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('name', unicode, name) self.name = name
self.add_field('members', list)
self.add_field('versions', list)
self.add_field('categories', list)
self.add_field('statuses', list)
def __repr__(self): def __repr__(self):
return '<Project %r>' % self.name return '<Project %r>' % self.name
def find_user(self, id, name): def find_user(self, id, name):
"""
Find a user from its ID.
If not found, create a :class:`User` with the specified name.
:param id: ID of user
:type id: str
:param name: Name of user
:type name: str
:rtype: :class:`User`
"""
for user in self.members: for user in self.members:
if user.id == id: if user.id == id:
return user return user
@ -49,6 +68,17 @@ class Project(CapBaseObject):
return User(id, name) return User(id, name)
def find_version(self, id, name): def find_version(self, id, name):
"""
Find a version from an ID.
If not found, create a :class:`Version` with the specified name.
:param id: ID of version
:type id: str
:param name: Name of version
:type name: str
:rtype: :class:`Version`
"""
for version in self.versions: for version in self.versions:
if version.id == id: if version.id == id:
return version return version
@ -57,6 +87,13 @@ class Project(CapBaseObject):
return Version(id, name) return Version(id, name)
def find_status(self, name): def find_status(self, name):
"""
Find a status from a name.
:param name: Name of status
:type name: str
:rtype: :class:`Status`
"""
for status in self.statuses: for status in self.statuses:
if status.name == name: if status.name == name:
return status return status
@ -65,98 +102,129 @@ class Project(CapBaseObject):
return None return None
class User(CapBaseObject): class User(CapBaseObject):
"""
User.
"""
name = StringField('Name of user')
def __init__(self, id, name): def __init__(self, id, name):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('name', unicode, name) self.name = name
def __repr__(self): def __repr__(self):
return '<User %r>' % self.name return '<User %r>' % self.name
class Version(CapBaseObject): class Version(CapBaseObject):
"""
Version of a project.
"""
name = StringField('Name of version')
def __init__(self, id, name): def __init__(self, id, name):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('name', unicode, name) self.name = name
def __repr__(self): def __repr__(self):
return '<Version %r>' % self.name return '<Version %r>' % self.name
class Status(CapBaseObject): class Status(CapBaseObject):
"""
Status of an issue.
**VALUE_** constants are the primary status
types.
"""
(VALUE_NEW, (VALUE_NEW,
VALUE_PROGRESS, VALUE_PROGRESS,
VALUE_RESOLVED, VALUE_RESOLVED,
VALUE_REJECTED) = range(4) VALUE_REJECTED) = range(4)
name = StringField('Name of status')
value = IntField('Value of status (constants VALUE_*)')
def __init__(self, id, name, value): def __init__(self, id, name, value):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('name', unicode, name) self.name = name
self.add_field('value', int, value) self.value = value
def __repr__(self): def __repr__(self):
return '<Status %r>' % self.name return '<Status %r>' % self.name
class Attachment(CapBaseObject): class Attachment(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Attachment of an issue.
self.add_field('filename', basestring) """
self.add_field('url', basestring) filename = StringField('Filename')
url = StringField('Direct URL to attachment')
def __repr__(self): def __repr__(self):
return '<Attachment %r>' % self.filename return '<Attachment %r>' % self.filename
class Change(CapBaseObject): class Change(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) A change of an update.
self.add_field('field', unicode) """
self.add_field('last', unicode) field = StringField('What field has been changed')
self.add_field('new', unicode) last = StringField('Last value of field')
new = StringField('New value of field')
class Update(CapBaseObject): class Update(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Represents an update of an issue.
self.add_field('author', User) """
self.add_field('date', datetime) author = Field('Author of update', User)
self.add_field('hours', timedelta) date = DateField('Date of update')
self.add_field('message', unicode) hours = DeltaField('Time activity')
self.add_field('attachments', (list,tuple)) # Attachment message = StringField('Log message')
self.add_field('changes', (list,tuple)) # Change attachments = Field('Files attached to update', list, tuple)
changes = Field('List of changes', list, tuple)
def __repr__(self): def __repr__(self):
return '<Update %r>' % self.id return '<Update %r>' % self.id
class Issue(CapBaseObject): class Issue(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Represents an issue.
self.add_field('project', Project) """
self.add_field('title', unicode) project = Field('Project of this issue', Project)
self.add_field('body', unicode) title = StringField('Title of issue')
self.add_field('creation', datetime) body = StringField('Text of issue')
self.add_field('updated', datetime) creation = DateField('Date when this issue has been created')
self.add_field('attachments', (list,tuple)) updated = DateField('Date when this issue has been updated for the last time')
self.add_field('history', (list,tuple)) attachments = Field('List of attached files', list, tuple)
self.add_field('author', User) history = Field('History of updates', list, tuple)
self.add_field('assignee', User) author = Field('Author of this issue', User)
self.add_field('category', unicode) assignee = Field('User assigned to this issue', User)
self.add_field('version', Version) category = StringField('Name of the category')
self.add_field('status', Status) version = Field('Target version of this issue', Version)
status = Field('Status of this issue', Status)
class Query(CapBaseObject): class Query(CapBaseObject):
"""
Query to find an issue.
"""
project = StringField('Filter on projects')
title = StringField('Filter on titles')
author = StringField('Filter on authors')
assignee = StringField('Filter on assignees')
version = StringField('Filter on versions')
category = StringField('Filter on categories')
status = StringField('Filter on statuses')
def __init__(self): def __init__(self):
CapBaseObject.__init__(self, '') CapBaseObject.__init__(self, '')
self.add_field('project', unicode)
self.add_field('title', unicode)
self.add_field('author', unicode)
self.add_field('assignee', unicode)
self.add_field('version', unicode)
self.add_field('category', unicode)
self.add_field('status', unicode)
class ICapBugTracker(IBaseCap): class ICapBugTracker(IBaseCap):
"""
Bug trackers websites.
"""
def iter_issues(self, query): def iter_issues(self, query):
""" """
Iter issues with optionnal patterns. Iter issues with optionnal patterns.
@param query [Query] :param query: query
@return [iter(Issue)] issues :type query: :class:`Query`
:rtype: iter[:class:`Issue`]
""" """
raise NotImplementedError() raise NotImplementedError()
@ -164,7 +232,8 @@ class ICapBugTracker(IBaseCap):
""" """
Get an issue from its ID. Get an issue from its ID.
@return Issue :param id: ID of issue
:rtype: :class:`Issue`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -172,13 +241,19 @@ class ICapBugTracker(IBaseCap):
""" """
Create an empty issue on the given project. Create an empty issue on the given project.
@return [Issue] the created issue. :param project: project
:type project: :class:`Project`
:returns: the created issue
:rtype: :class:`Issue`
""" """
raise NotImplementedError() raise NotImplementedError()
def post_issue(self, issue): def post_issue(self, issue):
""" """
Post an issue to create or update it. Post an issue to create or update it.
:param issue: issue to create or update
:type issue: :class:`Issue`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -186,14 +261,19 @@ class ICapBugTracker(IBaseCap):
""" """
Add an update to an issue. Add an update to an issue.
@param issue [id,Issue] issue or id of issue :param issue: issue or id of issue
@param update [Update] an Update object :type issue: :class:`Issue`
:param update: an Update object
:type update: :class:`Update`
""" """
raise NotImplementedError() raise NotImplementedError()
def remove_issue(self, issue): def remove_issue(self, issue):
""" """
Remove an issue. Remove an issue.
:param issue: issue
:type issue: :class:`Issue`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -201,7 +281,7 @@ class ICapBugTracker(IBaseCap):
""" """
Iter projects. Iter projects.
@return [iter(Project)] projects :rtype: iter[:class:`Project`]
""" """
raise NotImplementedError() raise NotImplementedError()
@ -209,6 +289,6 @@ class ICapBugTracker(IBaseCap):
""" """
Get a project from its ID. Get a project from its ID.
@return [Project] :rtype: :class:`Project`
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -20,31 +20,60 @@
import datetime import datetime
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, StringField, DateField
__all__ = ['ChatException', 'ICapChat'] __all__ = ['ChatException', 'ChatMessage', 'ICapChat']
class ChatException(Exception): class ChatException(Exception):
pass """
Exception raised when there is a problem with the chat.
"""
class ChatMessage(CapBaseObject): class ChatMessage(CapBaseObject):
"""
Message on the chat.
"""
id_from = StringField('ID of sender')
id_to = StringField('ID of recipient')
message = StringField('Content of message')
date = DateField('Date when the message has been sent')
def __init__(self, id_from, id_to, message, date=None): def __init__(self, id_from, id_to, message, date=None):
CapBaseObject.__init__(self, '%s.%s' % (id_from, id_to)) CapBaseObject.__init__(self, '%s.%s' % (id_from, id_to))
self.add_field('id_from', basestring, id_from) self.id_from = id_from
self.add_field('id_to', basestring, id_to) self.id_to = id_to
self.add_field('message', basestring, message) self.message = message
self.add_field('date', datetime.datetime, date) self.date = date
if self.date is None: if self.date is None:
self.date = datetime.datetime.utcnow() self.date = datetime.datetime.utcnow()
class ICapChat(IBaseCap): class ICapChat(IBaseCap):
"""
Websites with a chat system.
"""
def iter_chat_messages(self, _id=None): def iter_chat_messages(self, _id=None):
"""
Iter messages.
:param _id: optional parameter to only get messages
from a given contact.
:type _id: str
:rtype: iter[:class:`ChatMessage`]
"""
raise NotImplementedError() raise NotImplementedError()
def send_chat_message(self, _id, message): def send_chat_message(self, _id, message):
"""
Send a message to a contact.
:param _id: ID of recipient
:type _id: str
:param message: message to send
:type message: str
:raises: :class:`ChatException`
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,14 +18,17 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, Field, StringField, BytesField, IntField
from weboob.tools.ordereddict import OrderedDict from weboob.tools.ordereddict import OrderedDict
__all__ = ['ICapContact', 'Contact'] __all__ = ['ProfileNode', 'ContactPhoto', 'Contact', 'QueryError', 'Query', 'ICapContact']
class ProfileNode(object): class ProfileNode(object):
"""
Node of a :class:`Contact` profile.
"""
HEAD = 0x01 HEAD = 0x01
SECTION = 0x02 SECTION = 0x02
@ -41,14 +44,19 @@ class ProfileNode(object):
class ContactPhoto(CapBaseObject): class ContactPhoto(CapBaseObject):
"""
Photo of a contact.
"""
name = StringField('Name of the photo')
url = StringField('Direct URL to photo')
data = BytesField('Data of photo')
thumbnail_url = StringField('Direct URL to thumbnail')
thumbnail_data = BytesField('Data of thumbnail')
hidden = Field('True if the photo is hidden on website', bool)
def __init__(self, name): def __init__(self, name):
CapBaseObject.__init__(self, name) CapBaseObject.__init__(self, name)
self.add_field('name', basestring, name) self.name = name
self.add_field('url', basestring)
self.add_field('data', str)
self.add_field('thumbnail_url', basestring)
self.add_field('thumbnail_data', basestring)
self.add_field('hidden', bool, False)
def __iscomplete__(self): def __iscomplete__(self):
return (self.data and (not self.thumbnail_url or self.thumbnail_data)) return (self.data and (not self.thumbnail_url or self.thumbnail_data))
@ -63,22 +71,35 @@ class ContactPhoto(CapBaseObject):
class Contact(CapBaseObject): class Contact(CapBaseObject):
"""
A contact.
"""
STATUS_ONLINE = 0x001 STATUS_ONLINE = 0x001
STATUS_AWAY = 0x002 STATUS_AWAY = 0x002
STATUS_OFFLINE = 0x004 STATUS_OFFLINE = 0x004
STATUS_ALL = 0xfff STATUS_ALL = 0xfff
name = StringField('Name of contact')
status = IntField('Status of contact (STATUS_* constants)')
url = StringField('URL to the profile of contact')
status_msg = StringField('Message of status')
summary = StringField('Description of contact')
photos = Field('List of photos', dict, default=OrderedDict())
profile = Field('Contact profile', dict)
def __init__(self, id, name, status): def __init__(self, id, name, status):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('name', basestring, name) self.name = name
self.add_field('status', int, status) self.status = status
self.add_field('url', basestring)
self.add_field('status_msg', basestring)
self.add_field('summary', basestring)
self.add_field('photos', dict, OrderedDict())
self.add_field('profile', dict)
def set_photo(self, name, **kwargs): def set_photo(self, name, **kwargs):
"""
Set photo of contact.
:param name: name of photo
:type name: str
:param kwargs: See :class:`ContactPhoto` to know what other parameters you can use
"""
if not name in self.photos: if not name in self.photos:
self.photos[name] = ContactPhoto(name) self.photos[name] = ContactPhoto(name)
@ -88,13 +109,20 @@ class Contact(CapBaseObject):
class QueryError(Exception): class QueryError(Exception):
pass """
Raised when unable to send a query to a contact.
"""
class Query(CapBaseObject): class Query(CapBaseObject):
"""
Query to send to a contact.
"""
message = StringField('Message received')
def __init__(self, id, message): def __init__(self, id, message):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('message', basestring, message) self.message = message
class ICapContact(IBaseCap): class ICapContact(IBaseCap):
@ -102,9 +130,11 @@ class ICapContact(IBaseCap):
""" """
Iter contacts Iter contacts
@param status get only contacts with the specified status :param status: get only contacts with the specified status
@param ids if set, get the specified contacts :type status: Contact.STATUS_*
@return iterator over the contacts found :param ids: if set, get the specified contacts
:type ids: list[str]
:rtype: iter[:class:`Contact`]
""" """
raise NotImplementedError() raise NotImplementedError()
@ -116,8 +146,9 @@ class ICapContact(IBaseCap):
with the proper values, but it might be overloaded with the proper values, but it might be overloaded
by backends. by backends.
@param id the ID requested :param id: the ID requested
@return the Contact object, or None if not found :type id: str
:rtype: :class:`Contact` or None if not found
""" """
l = self.iter_contacts(ids=[id]) l = self.iter_contacts(ids=[id])
@ -130,9 +161,10 @@ class ICapContact(IBaseCap):
""" """
Send a query to a contact Send a query to a contact
@param id the ID of contact :param id: the ID of contact
@return a Query object :type id: str
@except QueryError :rtype: :class:`Query`
:raises: :class:`QueryError`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -140,8 +172,9 @@ class ICapContact(IBaseCap):
""" """
Get personal notes about a contact Get personal notes about a contact
@param id the ID of the contact :param id: the ID of the contact
@return a unicode object :type id: str
:rtype: unicode
""" """
raise NotImplementedError raise NotImplementedError
@ -149,7 +182,8 @@ class ICapContact(IBaseCap):
""" """
Set personal notes about a contact Set personal notes about a contact
@param id the ID of the contact :param id: the ID of the contact
@param notes the unicode object to save as notes :type id: str
:returns: the unicode object to save as notes
""" """
raise NotImplementedError raise NotImplementedError

View file

@ -18,37 +18,74 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, StringField, DateField, Field
from datetime import datetime
__all__ = ['Content', 'Revision', 'ICapContent']
class Content(CapBaseObject): class Content(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Content object.
self.add_field('title', basestring) """
self.add_field('author', basestring) title = StringField('Title of content')
self.add_field('content', basestring) author = StringField('Original author of content')
self.add_field('revision', basestring) content = StringField('Body')
revision = StringField('ID of revision')
class Revision(CapBaseObject): class Revision(CapBaseObject):
def __init__(self, _id): """
CapBaseObject.__init__(self, _id) Revision of a change on a content.
self.add_field('author', basestring) """
self.add_field('comment', basestring) author = StringField('Author of revision')
self.add_field('revision', basestring) comment = StringField('Comment log about revision')
self.add_field('timestamp', datetime) timestamp = DateField('Date of revision')
self.add_field('minor', bool) minor = Field('Is this change minor?', bool)
class ICapContent(IBaseCap): class ICapContent(IBaseCap):
def get_content(self, id, revision=None): def get_content(self, id, revision=None):
"""
Get a content from an ID.
:param id: ID of content
:type id: str
:param revision: if given, get the content at this revision
:type revision: :class:`Revision`
:rtype: :class:`Content`
"""
raise NotImplementedError() raise NotImplementedError()
def iter_revisions(self, id, max_results=10): def iter_revisions(self, id, max_results=10):
"""
Iter revisions of a content.
:param id: id of content
:type id: str
:param max_results: maximum results
:type max_results: int
:rtype: iter[:class:`Revision`]
"""
raise NotImplementedError() raise NotImplementedError()
def push_content(self, content, message=None, minor=False): def push_content(self, content, message=None, minor=False):
"""
Push a new revision of a content.
:param content: object to push
:type content: :class:`Content`
:param message: log message to associate to new revision
:type message: str
:param minor: this is a minor revision
:type minor: bool
"""
raise NotImplementedError() raise NotImplementedError()
def get_content_preview(self, content): def get_content_preview(self, content):
"""
Get a HTML preview of a content.
:param content: content object
:type content: :class:`Content`
:rtype: str
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,55 +18,103 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
import datetime from .base import IBaseCap, CapBaseObject, Field, StringField, DateField
from .base import IBaseCap, CapBaseObject
from .contact import Contact from .contact import Contact
__all__ = ['ICapDating'] __all__ = ['OptimizationNotFound', 'Optimization', 'Event', 'ICapDating']
class OptimizationNotFound(Exception): class OptimizationNotFound(Exception):
pass """
Raised when an optimization is not found.
"""
class Optimization(object): class Optimization(object):
# Configuration of optim can be made by Value*s in this dict. """
Optimization.
:var CONFIG: Configuration of optim can be made by
:class:`weboob.tools.value.Value` objects
in this dict.
"""
CONFIG = {} CONFIG = {}
def start(self): def start(self):
"""
Start optimization.
"""
raise NotImplementedError() raise NotImplementedError()
def stop(self): def stop(self):
"""
Stop optimization.
"""
raise NotImplementedError() raise NotImplementedError()
def is_running(self): def is_running(self):
"""
Know if the optimization is currently running.
:rtype: bool
"""
raise NotImplementedError() raise NotImplementedError()
def get_config(self): def get_config(self):
"""
Get config of this optimization.
:rtype: dict
"""
return None return None
def set_config(self, params): def set_config(self, params):
"""
Set config of this optimization.
:param params: parameters
:type params: dict
"""
raise NotImplementedError() raise NotImplementedError()
class Event(CapBaseObject): class Event(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) A dating event (for example a visite, a query received, etc.)
self.add_field('date', (datetime.datetime)) """
self.add_field('contact', Contact) date = DateField('Date of event')
self.add_field('type', basestring) contact = Field('Contact related to this event', Contact)
self.add_field('message', basestring) type = StringField('Type of event')
message = StringField('Message of the event')
class ICapDating(IBaseCap): class ICapDating(IBaseCap):
"""
Capability for dating websites.
"""
def init_optimizations(self): def init_optimizations(self):
"""
Initialization of optimizations.
"""
raise NotImplementedError() raise NotImplementedError()
def add_optimization(self, name, optim): def add_optimization(self, name, optim):
"""
Add an optimization.
:param name: name of optimization
:type name: str
:param optim: optimization
:type optim: :class:`Optimization`
"""
setattr(self, 'OPTIM_%s' % name, optim) setattr(self, 'OPTIM_%s' % name, optim)
def iter_optimizations(self, *optims): def iter_optimizations(self):
"""
Iter optimizations.
:rtype: iter[:class:`Optimization`]
"""
for attr_name in dir(self): for attr_name in dir(self):
if not attr_name.startswith('OPTIM_'): if not attr_name.startswith('OPTIM_'):
continue continue
@ -77,6 +125,13 @@ class ICapDating(IBaseCap):
yield attr_name[6:], attr yield attr_name[6:], attr
def get_optimization(self, optim): def get_optimization(self, optim):
"""
Get an optimization from a name.
:param optim: name of optimization
:type optim: str
:rtype: :class:`Optimization`
"""
optim = optim.upper() optim = optim.upper()
if not hasattr(self, 'OPTIM_%s' % optim): if not hasattr(self, 'OPTIM_%s' % optim):
raise OptimizationNotFound() raise OptimizationNotFound()
@ -84,4 +139,9 @@ class ICapDating(IBaseCap):
return getattr(self, 'OPTIM_%s' % optim) return getattr(self, 'OPTIM_%s' % optim)
def iter_events(self): def iter_events(self):
"""
Iter events.
:rtype: iter[:class:`Event`]
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -17,31 +17,39 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from weboob.tools.capabilities.thumbnail import Thumbnail from weboob.tools.capabilities.thumbnail import Thumbnail
from .base import IBaseCap, CapBaseObject, NotLoaded from .base import IBaseCap, CapBaseObject, NotLoaded, Field, StringField, \
BytesField, IntField, FloatField, DateField
__all__ = ['Thumbnail', 'ICapGallery', 'BaseGallery', 'BaseImage'] __all__ = ['BaseGallery', 'BaseImage', 'ICapGallery']
class BaseGallery(CapBaseObject): class BaseGallery(CapBaseObject):
""" """
Represents a gallery. Represents a gallery.
This object has to be inherited to specify how to calculate the URL of the gallery from its ID. This object has to be inherited to specify how to calculate the URL of the gallery from its ID.
""" """
title = StringField('Title of gallery')
url = StringField('Direct URL to gallery')
description = StringField('Description of gallery')
cardinality = IntField('Cardinality of gallery')
date = DateField('Date of gallery')
rating = FloatField('Rating of this gallery')
rating_max = FloatField('Max rating available')
thumbnail = Field('Thumbnail', Thumbnail)
def __init__(self, _id, title=NotLoaded, url=NotLoaded, cardinality=NotLoaded, date=NotLoaded, def __init__(self, _id, title=NotLoaded, url=NotLoaded, cardinality=NotLoaded, date=NotLoaded,
rating=NotLoaded, rating_max=NotLoaded, thumbnail=NotLoaded, thumbnail_url=None, nsfw=False): rating=NotLoaded, rating_max=NotLoaded, thumbnail=NotLoaded, thumbnail_url=None, nsfw=False):
CapBaseObject.__init__(self, unicode(_id)) CapBaseObject.__init__(self, unicode(_id))
self.add_field('title', basestring, title) self.title = title
self.add_field('url', basestring, url) self.url = url
self.add_field('description', basestring) self.date = date
self.add_field('cardinality', int) self.rating = rating
self.add_field('date', datetime, date) self.rating_max = rating_max
self.add_field('rating', (int, long, float), rating) self.thumbnail = thumbnail
self.add_field('rating_max', (int, long, float), rating_max)
self.add_field('thumbnail', Thumbnail, thumbnail)
@classmethod @classmethod
def id2url(cls, _id): def id2url(cls, _id):
@ -50,24 +58,39 @@ class BaseGallery(CapBaseObject):
@property @property
def page_url(self): def page_url(self):
"""
Get URL to page of this gallery.
"""
return self.id2url(self.id) return self.id2url(self.id)
def iter_image(self): def iter_image(self):
"""
Iter images.
"""
raise NotImplementedError() raise NotImplementedError()
class BaseImage(CapBaseObject): class BaseImage(CapBaseObject):
"""
Base class for images.
"""
index = IntField('Usually page number')
thumbnail = Field('Thumbnail of the image', Thumbnail)
url = StringField('Direct URL to image')
ext = StringField('Extension of image')
data = BytesField('Data of image')
gallery = Field('Reference to the Gallery object', BaseGallery)
def __init__(self, _id, index=None, thumbnail=NotLoaded, url=NotLoaded, def __init__(self, _id, index=None, thumbnail=NotLoaded, url=NotLoaded,
ext=NotLoaded, gallery=None): ext=NotLoaded, gallery=None):
CapBaseObject.__init__(self, unicode(_id)) CapBaseObject.__init__(self, unicode(_id))
self.add_field('index', int, index) # usually page number self.index = index
self.add_field('thumbnail', Thumbnail, thumbnail) self.thumbnail = thumbnail
self.add_field('url', basestring, url) self.url = url
self.add_field('ext', basestring, ext) self.ext = ext
self.add_field('data', str) self.gallery = gallery
self.add_field('gallery', BaseGallery, gallery)
def __str__(self): def __str__(self):
return self.url return self.url
@ -92,10 +115,15 @@ class ICapGallery(IBaseCap):
""" """
Iter results of a search on a pattern. Iter results of a search on a pattern.
@param pattern [str] pattern to search on :param pattern: pattern to search on
@param sortby [enum] sort by... :type pattern: str
@param nsfw [bool] include non-suitable for work videos if True :param sortby: sort by...
@param max_results [int] maximum number of results to return :type sortby: SEARCH_*
:param nsfw: include non-suitable for work videos if True
:type nsfw: bool
:param max_results: maximum number of results to return
:type max_results: int
:rtype: :class:`BaseGallery`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -103,7 +131,8 @@ class ICapGallery(IBaseCap):
""" """
Get gallery from an ID. Get gallery from an ID.
@param _id the gallery id. It can be a numeric ID, or a page url, or so. :param _id: the gallery id. It can be a numeric ID, or a page url, or so.
@return a Gallery object :type _id: str
:rtype: :class:`Gallery`
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -16,39 +16,42 @@
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from datetime import datetime from .base import IBaseCap, CapBaseObject, StringField, FloatField, DateField
from .base import IBaseCap, CapBaseObject
__all__ = ['ICapWaterLevel'] __all__ = ['Gauge', 'GaugeMeasure', 'ICapWaterLevel']
class Gauge(CapBaseObject): class Gauge(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Gauge class.
"""
self.add_field('name', basestring) name = StringField('Name of gauge')
self.add_field('river', basestring) river = StringField('What river')
self.add_field('level', float) level = FloatField('Level of gauge')
self.add_field('flow', float) flow = FloatField('Flow of gauge')
self.add_field('lastdate', datetime) lastdate = DateField('Last measure')
self.add_field('forecast', basestring) forecast = StringField('Forecast')
class GaugeMeasure(CapBaseObject): class GaugeMeasure(CapBaseObject):
"""
Measure of a gauge.
"""
level = FloatField('Level of measure')
flow = FloatField('Flow of measure')
date = DateField('Date of measure')
def __init__(self): def __init__(self):
CapBaseObject.__init__(self, None) CapBaseObject.__init__(self, None)
self.add_field('level', float)
self.add_field('flow', float)
self.add_field('date', datetime)
class ICapWaterLevel(IBaseCap): class ICapWaterLevel(IBaseCap):
def iter_gauge_history(self, id): def iter_gauge_history(self, id):
""" """
Get history of a gauge. Get history of a gauge.
@param id [str] ID of the river :param id: ID of the river
@return [iter(GaugeMeasure)] :type id: str
:rtype: iter[:class:`GaugeMeasure`]
""" """
raise NotImplementedError() raise NotImplementedError()
@ -56,8 +59,9 @@ class ICapWaterLevel(IBaseCap):
""" """
Get last measure of the gauge. Get last measure of the gauge.
@param id [str] ID of the gauge. :param id: ID of the gauge
@return [GaugeMeasure] :type id: str
:rtype: :class:`GaugeMeasure`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -65,7 +69,8 @@ class ICapWaterLevel(IBaseCap):
""" """
Iter gauges. Iter gauges.
@param pattern [str] if specified, used to search gauges :param pattern: if specified, used to search gauges
@return [iter(Gauge)] :type pattern: str
:rtype: iter[:class:`Gauge`]
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,27 +18,40 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, StringField, FloatField
__all__ = ['IpLocation', 'ICapGeolocIp'] __all__ = ['IpLocation', 'ICapGeolocIp']
class IpLocation(CapBaseObject): class IpLocation(CapBaseObject):
"""
Represents the location of an IP address.
"""
city = StringField('City')
region = StringField('Region')
zipcode = StringField('Zip code')
country = StringField('Country')
lt = FloatField('Latitude')
lg = FloatField('Longitude')
host = StringField('Hostname')
tld = StringField('Top Level Domain')
isp = StringField('Internet Service Provider')
def __init__(self, ipaddr): def __init__(self, ipaddr):
CapBaseObject.__init__(self, ipaddr) CapBaseObject.__init__(self, ipaddr)
self.ipaddr = ipaddr self.ipaddr = ipaddr
self.add_field('city', basestring)
self.add_field('region', basestring)
self.add_field('zipcode', basestring)
self.add_field('country', basestring)
self.add_field('lt', float)
self.add_field('lg', float)
self.add_field('host', basestring)
self.add_field('tld', basestring)
self.add_field('isp', basestring)
class ICapGeolocIp(IBaseCap): class ICapGeolocIp(IBaseCap):
"""
Access information about IP addresses database.
"""
def get_location(self, ipaddr): def get_location(self, ipaddr):
"""
Get location of an IP address.
:param ipaddr: IP address
:type ipaddr: str
:rtype: :class:`IpLocation`
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,19 +18,23 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import date from .base import IBaseCap, CapBaseObject, Field, IntField, FloatField, \
StringField, BytesField, DateField
from .base import IBaseCap, CapBaseObject
__all__ = ['ICapHousing'] __all__ = ['HousingPhoto', 'Housing', 'Query', 'City', 'ICapHousing']
class HousingPhoto(CapBaseObject): class HousingPhoto(CapBaseObject):
"""
Photo of a housing.
"""
url = StringField('Direct URL to photo')
data = BytesField('Data of photo')
def __init__(self, url): def __init__(self, url):
CapBaseObject.__init__(self, url.split('/')[-1]) CapBaseObject.__init__(self, url.split('/')[-1])
self.add_field('url', basestring, url) self.url = url
self.add_field('data', str)
def __iscomplete__(self): def __iscomplete__(self):
return self.data return self.data
@ -42,45 +46,75 @@ class HousingPhoto(CapBaseObject):
return u'<HousingPhoto "%s" data=%do>' % (self.id, len(self.data) if self.data else 0) return u'<HousingPhoto "%s" data=%do>' % (self.id, len(self.data) if self.data else 0)
class Housing(CapBaseObject): class Housing(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Content of a housing.
self.add_field('title', basestring) """
self.add_field('area', (int,float)) title = StringField('Title of housing')
self.add_field('cost', (int,float)) area = FloatField('Area of housing, in m2')
self.add_field('currency', basestring) cost = FloatField('Cost of housing')
self.add_field('date', date) currency = StringField('Currency of cost')
self.add_field('location', basestring) date = DateField('Date when the housing has been published')
self.add_field('station', basestring) location = StringField('Location of housing')
self.add_field('text', basestring) station = StringField('What metro/bus station next to housing')
self.add_field('phone', basestring) text = StringField('Text of the housing')
self.add_field('photos', list) phone = StringField('Phone number to contact')
self.add_field('details', dict) photos = Field('List of photos', list)
details = Field('Key/values of details', dict)
class Query(CapBaseObject): class Query(CapBaseObject):
"""
Query to find housings.
"""
TYPE_RENT = 0 TYPE_RENT = 0
TYPE_SALE = 1 TYPE_SALE = 1
type = IntField('Type of housing to find (TYPE_* constants)')
cities = Field('List of cities to search in', list, tuple)
area_min = IntField('Minimal area (in m2)')
area_max = IntField('Maximal area (in m2)')
cost_min = IntField('Minimal cost')
cost_max = IntField('Maximal cost')
nb_rooms = IntField('Number of rooms')
def __init__(self): def __init__(self):
CapBaseObject.__init__(self, '') CapBaseObject.__init__(self, '')
self.add_field('type', int)
self.add_field('cities', (list,tuple))
self.add_field('area_min', int)
self.add_field('area_max', int)
self.add_field('cost_min', int)
self.add_field('cost_max', int)
self.add_field('nb_rooms', int)
class City(CapBaseObject): class City(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) City.
self.add_field('name', basestring) """
name = StringField('Name of city')
class ICapHousing(IBaseCap): class ICapHousing(IBaseCap):
"""
Capability of websites to search housings.
"""
def search_housings(self, query): def search_housings(self, query):
"""
Search housings.
:param query: search query
:type query: :class:`Query`
:rtype: iter[:class:`Housing`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_housing(self, housing): def get_housing(self, housing):
"""
Get an housing from an ID.
:param housing: ID of the housing
:type housing: str
:rtype: :class:`Housing` or None if not found.
"""
raise NotImplementedError() raise NotImplementedError()
def search_city(self, pattern): def search_city(self, pattern):
"""
Search a city from a pattern.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`City`]
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -17,40 +17,59 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, date
from .collection import ICapCollection from .collection import ICapCollection
from .base import CapBaseObject from .base import CapBaseObject, Field, StringField, DateField
__all__ = ['ICapBook', 'Book'] __all__ = ['Book', 'Renew', 'ICapBook']
class Book(CapBaseObject): class Book(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Describes a book.
self.add_field('name', basestring) """
self.add_field('author', basestring) name = StringField('Name of the book')
self.add_field('location', basestring) author = StringField('Author of the book')
self.add_field('date', (datetime, date)) # which may be the due date location = StringField('Location')
self.add_field('late', bool) date = DateField('The due date')
late = Field('Are you late?', bool)
class Renew(CapBaseObject): class Renew(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) A renew message.
self.add_field('message', basestring) """
message = StringField('Message')
class ICapBook(ICapCollection): class ICapBook(ICapCollection):
"""
Library websites.
"""
def iter_resources(self, objs, split_path): def iter_resources(self, objs, split_path):
"""
Iter resources. It retuns :func:`iter_books`.
"""
if Book in objs: if Book in objs:
self._restrict_level(split_path) self._restrict_level(split_path)
return self.iter_books() return self.iter_books()
def iter_books(self, pattern): def iter_books(self, pattern):
"""
Iter books from a pattern.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Book`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_book(self, _id): def get_book(self, _id):
"""
Get a book from an ID.
:param _id: ID of the book
:type _id: str
:rtype: :class:`Book`
"""
raise NotImplementedError() raise NotImplementedError()
def get_booked(self, _id): def get_booked(self, _id):

View file

@ -21,18 +21,42 @@
import datetime import datetime
import time import time
from .base import IBaseCap, CapBaseObject, NotLoaded from .base import IBaseCap, CapBaseObject, NotLoaded, Field, StringField, DateField, IntField
__all__ = ['ICapMessages', 'ICapMessagesPost', 'Message', 'Thread', 'CantSendMessage'] __all__ = ['Thread', 'Message', 'ICapMessages', 'CantSendMessage', 'ICapMessagesPost']
class Message(CapBaseObject): # Message and Thread's attributes refer to themselves, and it isn't possible
# in python, so these base classes are used instead.
class _Message(CapBaseObject):
""" Base message. """
pass
class _Thread(CapBaseObject):
""" Base Thread. """
pass
class Message(_Message):
"""
Represents a message read or to send.
"""
IS_HTML = 0x001 # The content is HTML formatted IS_HTML = 0x001 # The content is HTML formatted
IS_UNREAD = 0x002 # The message is unread IS_UNREAD = 0x002 # The message is unread
IS_RECEIVED = 0x004 # The receiver has read this message IS_RECEIVED = 0x004 # The receiver has read this message
IS_NOT_RECEIVED = 0x008 # The receiver has not read this message IS_NOT_RECEIVED = 0x008 # The receiver has not read this message
thread = Field('Reference to the thread', _Thread)
title = StringField('Title of message')
sender = StringField('Author of this message')
receivers = Field('Receivers of the message', list)
date = DateField('Date when the message has been sent')
content = StringField('Body of message')
signature = StringField('Optional signature')
parent = Field('Parent message', _Message)
children = Field('Children fields', list)
flags = IntField('Flags (IS_* constants)', default=0)
def __init__(self, thread, id, def __init__(self, thread, id,
title=NotLoaded, title=NotLoaded,
sender=NotLoaded, sender=NotLoaded,
@ -45,16 +69,14 @@ class Message(CapBaseObject):
flags=0): flags=0):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
assert thread is not None assert thread is not None
self.add_field('thread', Thread, thread) self.thread = thread
self.add_field('title', basestring, title) self.title = title
self.add_field('sender', basestring, sender) self.sender = sender
self.add_field('receivers', list, receivers) self.receivers = receivers
self.add_field('date', (datetime.datetime, datetime.date), date) self.content = content
self.add_field('parent', Message, parent) self.signature = signature
self.add_field('content', basestring, content) self.children = children
self.add_field('signature', basestring, signature) self.flags = flags
self.add_field('children', list, children)
self.add_field('flags', int, flags)
if date is None: if date is None:
date = datetime.datetime.utcnow() date = datetime.datetime.utcnow()
@ -68,14 +90,23 @@ class Message(CapBaseObject):
@property @property
def date_int(self): def date_int(self):
"""
Date of message as an integer.
"""
return int(time.strftime('%Y%m%d%H%M%S', self.date.timetuple())) return int(time.strftime('%Y%m%d%H%M%S', self.date.timetuple()))
@property @property
def full_id(self): def full_id(self):
"""
Full ID of message (in form '**THREAD_ID.MESSAGE_ID**')
"""
return '%s.%s' % (self.thread.id, self.id) return '%s.%s' % (self.thread.id, self.id)
@property @property
def full_parent_id(self): def full_parent_id(self):
"""
Get the full ID of the parent message (in form '**THREAD_ID.MESSAGE_ID**').
"""
if self.parent: if self.parent:
return self.parent.full_id return self.parent.full_id
elif self._parent_id is None: elif self._parent_id is None:
@ -96,18 +127,24 @@ class Message(CapBaseObject):
return '<Message id=%r title=%r date=%r from=%r>' % ( return '<Message id=%r title=%r date=%r from=%r>' % (
self.full_id, self.title, self.date, self.sender) self.full_id, self.title, self.date, self.sender)
class Thread(CapBaseObject): class Thread(_Thread):
"""
Thread containing messages.
"""
IS_THREADS = 0x001 IS_THREADS = 0x001
IS_DISCUSSION = 0x002 IS_DISCUSSION = 0x002
def __init__(self, id): root = Field('Root message', Message)
CapBaseObject.__init__(self, id) title = StringField('Title of thread')
self.add_field('root', Message) date = DateField('Date of thread')
self.add_field('title', basestring) flags = IntField('Flags (IS_* constants)', default=0)
self.add_field('date', (datetime.datetime, datetime.date))
self.add_field('flags', int, self.IS_THREADS)
def iter_all_messages(self): def iter_all_messages(self):
"""
Iter all messages of the thread.
:rtype: iter[:class:`Message`]
"""
if self.root: if self.root:
yield self.root yield self.root
for m in self._iter_all_messages(self.root): for m in self._iter_all_messages(self.root):
@ -122,11 +159,14 @@ class Thread(CapBaseObject):
class ICapMessages(IBaseCap): class ICapMessages(IBaseCap):
"""
Capability to read messages.
"""
def iter_threads(self): def iter_threads(self):
""" """
Iterates on threads, from newers to olders. Iterates on threads, from newers to olders.
@return [iter] Thread objects :rtype: iter[:class:`Thread`]
""" """
raise NotImplementedError() raise NotImplementedError()
@ -134,7 +174,7 @@ class ICapMessages(IBaseCap):
""" """
Get a specific thread. Get a specific thread.
@return [Thread] the Thread object :rtype: :class:`Thread`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -142,7 +182,7 @@ class ICapMessages(IBaseCap):
""" """
Iterates on messages which hasn't been marked as read. Iterates on messages which hasn't been marked as read.
@return [iter] Message objects :rtype: iter[:class:`Message`]
""" """
raise NotImplementedError() raise NotImplementedError()
@ -150,19 +190,26 @@ class ICapMessages(IBaseCap):
""" """
Set a message as read. Set a message as read.
@param [message] message read (or ID) :param message: message read (or ID)
:type message: :class:`Message` or str
""" """
raise NotImplementedError() raise NotImplementedError()
class CantSendMessage(Exception): class CantSendMessage(Exception):
pass """
Raised when a message can't be send.
"""
class ICapMessagesPost(IBaseCap): class ICapMessagesPost(IBaseCap):
"""
This capability allow user to send a message.
"""
def post_message(self, message): def post_message(self, message):
""" """
Post a message. Post a message.
@param message Message object :param message: message to send
@return :type message: :class:`Message`
:raises: :class:`CantSendMessage`
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,27 +18,34 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject, NotLoaded from .base import IBaseCap, CapBaseObject, NotLoaded, Field, StringField
__all__ = ['PasteNotFound', 'BasePaste', 'ICapPaste'] __all__ = ['PasteNotFound', 'BasePaste', 'ICapPaste']
class PasteNotFound(Exception): class PasteNotFound(Exception):
pass """
Raised when a paste is not found.
"""
class BasePaste(CapBaseObject): class BasePaste(CapBaseObject):
""" """
Represents a pasted text. Represents a pasted text.
""" """
title = StringField('Title of paste')
language = StringField('Language of the paste')
contents = StringField('Content of the paste')
public = Field('Is this paste public?', bool)
def __init__(self, _id, title=NotLoaded, language=NotLoaded, contents=NotLoaded, def __init__(self, _id, title=NotLoaded, language=NotLoaded, contents=NotLoaded,
public=NotLoaded): public=NotLoaded):
CapBaseObject.__init__(self, unicode(_id)) CapBaseObject.__init__(self, unicode(_id))
self.add_field('title', basestring, title) self.title = title
self.add_field('language', basestring, language) self.language = language
self.add_field('contents', basestring, contents) self.contents = contents
self.add_field('public', bool, public) self.public = public
@classmethod @classmethod
def id2url(cls, _id): def id2url(cls, _id):
@ -47,6 +54,9 @@ class BasePaste(CapBaseObject):
@property @property
def page_url(self): def page_url(self):
"""
Get URL to page of this paste.
"""
return self.id2url(self.id) return self.id2url(self.id)
@ -60,7 +70,7 @@ class ICapPaste(IBaseCap):
Get a new paste object for posting it with the backend. Get a new paste object for posting it with the backend.
The parameters should be passed to the object init. The parameters should be passed to the object init.
@return a Paste object :rtype: :class:`BasePaste`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -79,7 +89,8 @@ class ICapPaste(IBaseCap):
A score of 1 means the backend is suitable. A score of 1 means the backend is suitable.
Higher scores means it is more suitable than others with a lower score. Higher scores means it is more suitable than others with a lower score.
@return int Score :rtype: int
:returns: score
""" """
raise NotImplementedError() raise NotImplementedError()
@ -87,8 +98,10 @@ class ICapPaste(IBaseCap):
""" """
Get a Paste from an ID or URL. Get a Paste from an ID or URL.
@param _id the paste id. It can be an ID or a page URL. :param _id: the paste id. It can be an ID or a page URL.
@return a Paste object :type _id: str
:rtype: :class:`BasePaste`
:raises: :class:`PasteNotFound`
""" """
raise NotImplementedError() raise NotImplementedError()
@ -96,7 +109,7 @@ class ICapPaste(IBaseCap):
""" """
Post a paste. Post a paste.
@param paste Paste object :param paste: a Paste object
@return :type paste: :class:`BasePaste`
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,17 +18,18 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, Field, StringField
__all__ = ['Emission', 'Stream', 'Radio', 'ICapRadio'] __all__ = ['Emission', 'Stream', 'Radio', 'ICapRadio']
class Emission(CapBaseObject): class Emission(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Emission of a radio.
self.add_field('artist', unicode) """
self.add_field('title', unicode) artist = StringField('Name of artist')
title = StringField('Title of song or emission')
def __iscomplete__(self): def __iscomplete__(self):
# This volatile information may be reloaded everytimes. # This volatile information may be reloaded everytimes.
@ -41,10 +42,11 @@ class Emission(CapBaseObject):
return self.title return self.title
class Stream(CapBaseObject): class Stream(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Stream of a radio.
self.add_field('title', unicode) """
self.add_field('url', unicode) title = StringField('Title of stream')
url = StringField('Direct URL to the stream')
def __unicode__(self): def __unicode__(self):
return u'%s (%s)' % (self.title, self.url) return u'%s (%s)' % (self.title, self.url)
@ -53,16 +55,34 @@ class Stream(CapBaseObject):
return self.__unicode__() return self.__unicode__()
class Radio(CapBaseObject): class Radio(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) Radio object.
self.add_field('title', unicode) """
self.add_field('description', unicode) title = StringField('Title of radio')
self.add_field('current', Emission) description = StringField('Description of radio')
self.add_field('streams', list) current = Field('Current emission', Emission)
streams = Field('List of streams', list)
class ICapRadio(IBaseCap): class ICapRadio(IBaseCap):
"""
Capability of radio websites.
"""
def iter_radios_search(self, pattern): def iter_radios_search(self, pattern):
"""
Search a radio.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Radio`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_radio(self, id): def get_radio(self, id):
"""
Get a radio from an ID.
:param id: ID of radio
:type id: str
:rtype: :class:`Radio`
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -17,41 +17,72 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, Field, StringField, FloatField, DateField, IntField
__all__ = ['ICapTorrent', 'Torrent'] __all__ = ['MagnetOnly', 'Torrent', 'ICapTorrent']
class MagnetOnly(Exception): class MagnetOnly(Exception):
"""
Raised when trying to get URL to torrent but only magnet is available.
"""
def __init__(self, magnet): def __init__(self, magnet):
self.magnet = magnet self.magnet = magnet
Exception.__init__(self, 'Only magnet URL is available') Exception.__init__(self, 'Only magnet URL is available')
class Torrent(CapBaseObject): class Torrent(CapBaseObject):
"""
Torrent object.
"""
name = StringField('Name of torrent')
size = FloatField('Size of torrent')
date = DateField('Date when torrent has been published')
url = StringField('Direct url to .torrent file')
magnet = StringField('URI of magnet')
seeders = IntField('Number of seeders')
leechers = IntField('Number of leechers')
files = Field('Files in torrent', list)
description = StringField('Description of torrent')
filename = StringField('Name of .torrent file')
def __init__(self, id, name): def __init__(self, id, name):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('name', basestring, name) self.name = name
self.add_field('size', (int, long, float))
self.add_field('date', datetime)
self.add_field('url', basestring)
self.add_field('magnet', basestring)
self.add_field('seeders', int)
self.add_field('leechers', int)
self.add_field('files', list)
self.add_field('description', basestring)
self.add_field('filename', basestring) # suggested name of the .torrent file
class ICapTorrent(IBaseCap): class ICapTorrent(IBaseCap):
"""
Torrent trackers.
"""
def iter_torrents(self, pattern): def iter_torrents(self, pattern):
"""
Search torrents and iterate on results.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`Torrent`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_torrent(self, _id): def get_torrent(self, _id):
"""
Get a torrent object from an ID.
:param _id: ID of torrent
:type _id: str
:rtype: :class:`Torrent`
"""
raise NotImplementedError() raise NotImplementedError()
def get_torrent_file(self, _id): def get_torrent_file(self, _id):
"""
Get the content of the .torrent file.
:param _id: ID of torrent
:type _id: str
:rtype: str
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -18,76 +18,98 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import time, datetime, timedelta import datetime
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, StringField, TimeField, DeltaField, DateField
__all__ = ['Departure', 'ICapTravel', 'Station'] __all__ = ['Station', 'Departure', 'RoadStep', 'RoadmapError', 'RoadmapFilters', 'ICapTravel']
class Station(CapBaseObject): class Station(CapBaseObject):
"""
Describes a station.
"""
name = StringField('Name of station')
def __init__(self, id, name): def __init__(self, id, name):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('name', basestring, name) self.name = name
def __repr__(self): def __repr__(self):
return "<Station id=%r name=%r>" % (self.id, self.name) return "<Station id=%r name=%r>" % (self.id, self.name)
class Departure(CapBaseObject): class Departure(CapBaseObject):
"""
Describes a departure.
"""
type = StringField('Type of train')
time = TimeField('When the train will leave')
departure_station = StringField('Departure station')
arrival_station = StringField('Destination of the train')
late = TimeField('Optional late', default=datetime.time())
information = StringField('Informations')
plateform = StringField('Where the train will leave')
def __init__(self, id, _type, _time): def __init__(self, id, _type, _time):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('type', basestring, _type) self.type = _type
self.add_field('time', datetime, _time) self.time = _time
self.add_field('departure_station', basestring)
self.add_field('arrival_station', basestring)
self.add_field('late', time, time())
self.add_field('information', basestring)
self.add_field('plateform', basestring)
def __repr__(self): def __repr__(self):
return u"<Departure id=%r type=%r time=%r departure=%r arrival=%r>" % ( return u"<Departure id=%r type=%r time=%r departure=%r arrival=%r>" % (
self.id, self.type, self.time.strftime('%H:%M'), self.departure_station, self.arrival_station) self.id, self.type, self.time.strftime('%H:%M'), self.departure_station, self.arrival_station)
class RoadStep(CapBaseObject): class RoadStep(CapBaseObject):
def __init__(self, id): """
CapBaseObject.__init__(self, id) A step on a roadmap.
"""
self.add_field('line', basestring) line = StringField('When line')
self.add_field('start_time', time) start_time = TimeField('Start of step')
self.add_field('end_time', time) end_time = TimeField('End of step')
self.add_field('departure', unicode) departure = StringField('Departure station')
self.add_field('arrival', unicode) arrival = StringField('Arrival station')
self.add_field('duration', timedelta) duration = DeltaField('Duration of this step')
class RoadmapError(Exception): class RoadmapError(Exception):
pass """
Raised when the roadmap is unable to be calculated.
"""
class RoadmapFilters(CapBaseObject): class RoadmapFilters(CapBaseObject):
"""
Filters to get a roadmap.
"""
departure_time = DateField('Wanted departure time')
arrival_time = DateField('Wanted arrival time')
def __init__(self): def __init__(self):
CapBaseObject.__init__(self, '') CapBaseObject.__init__(self, '')
self.add_field('departure_time', datetime)
self.add_field('arrival_time', datetime)
class ICapTravel(IBaseCap): class ICapTravel(IBaseCap):
"""
Travel websites.
"""
def iter_station_search(self, pattern): def iter_station_search(self, pattern):
""" """
Iterates on search results of stations. Iterates on search results of stations.
@param pattern [str] the search pattern :param pattern: the search pattern
@return [iter] the of Station objects :type pattern: str
:rtype: iter[:class:`Station`]
""" """
raise NotImplementedError() raise NotImplementedError()
def iter_station_departures(self, station_id, arrival_id): def iter_station_departures(self, station_id, arrival_id=None):
""" """
Iterate on departures. Iterate on departures.
@param station_id [id] the station id :param station_id: the station ID
@param arrival_id [id] optionnal arrival station id :type station_id: str
@return [iter] result of Departure objects :param arrival_id: optionnal arrival station ID
:type arrival_id: str
:rtype: iter[:class:`Departure`]
""" """
raise NotImplementedError() raise NotImplementedError()
@ -95,9 +117,12 @@ class ICapTravel(IBaseCap):
""" """
Get a roadmap. Get a roadmap.
@param departure [str] name of departure station :param departure: name of departure station
@param arrival [str] name of arrival station :type departure: str
@param filters [RoadmapFilters] filters on search :param arrival: name of arrival station
@return [iter(RoadStep)] steps of roadmap :type arrival: str
:param filters: filters on search
:type filters: :class:`RoadmapFilters`
:rtype: iter[:class:`RoadStep`]
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright(C) 2010-2011 Romain Bignon, Christophe Benz # Copyright(C) 2010-2012 Romain Bignon, Christophe Benz
# #
# This file is part of weboob. # This file is part of weboob.
# #
@ -18,34 +18,33 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, timedelta from datetime import timedelta
from .base import IBaseCap, CapBaseObject, NotAvailable from .base import IBaseCap, CapBaseObject, NotAvailable, StringField, Field, DateField
from weboob.tools.capabilities.thumbnail import Thumbnail from weboob.tools.capabilities.thumbnail import Thumbnail
__all__ = ['BaseVideo', 'ICapVideo'] __all__ = ['BaseVideo', 'ICapVideo']
class BaseVideo(CapBaseObject): class BaseVideo(CapBaseObject):
""" """
Represents a video. Represents a video.
This object has to be inherited to specify how to calculate the URL of the video from its ID. This object has to be inherited to specify how to calculate the URL of the video from its ID.
""" """
def __init__(self, _id): title = StringField('Title of video')
CapBaseObject.__init__(self, unicode(_id)) url = StringField('URL to the video file')
ext = StringField('Extension of video')
self.add_field('title', basestring) author = StringField('Author of video')
self.add_field('url', basestring) description = StringField('Description of video')
self.add_field('ext', basestring) duration = Field('Duration of video', int, long, timedelta)
self.add_field('author', basestring) date = DateField('Date when the video has been published')
self.add_field('description', basestring) rating = Field('Rating of video', int, long, float, default=NotAvailable)
self.add_field('duration', (int,long,timedelta)) rating_max = Field('Max rating', int, long, float, default=NotAvailable)
self.add_field('date', datetime) thumbnail = Field('Thumbnail of video', Thumbnail)
self.add_field('rating', (int,long,float), NotAvailable) nsfw = Field('Is this video Not Safe For Work', bool, default=False)
self.add_field('rating_max', (int,long,float), NotAvailable)
self.add_field('thumbnail', Thumbnail)
self.add_field('nsfw', bool, False)
@classmethod @classmethod
def id2url(cls, _id): def id2url(cls, _id):
@ -54,6 +53,9 @@ class BaseVideo(CapBaseObject):
@property @property
def page_url(self): def page_url(self):
"""
Get page URL of the video.
"""
return self.id2url(self.id) return self.id2url(self.id)
@ -70,10 +72,14 @@ class ICapVideo(IBaseCap):
""" """
Iter results of a search on a pattern. Iter results of a search on a pattern.
@param pattern [str] pattern to search on :param pattern: pattern to search on
@param sortby [enum] sort by... :type pattern: str
@param nsfw [bool] include non-suitable for work videos if True :param sortby: sort by... (use SEARCH_* constants)
@param max_results [int] maximum number of results to return :param nsfw: include non-suitable for work videos if True
:type nsfw: bool
:param max_results: maximum number of results to return
:type max_results: int
:rtype: iter[:class:`BaseVideo`]
""" """
raise NotImplementedError() raise NotImplementedError()
@ -81,7 +87,8 @@ class ICapVideo(IBaseCap):
""" """
Get a Video from an ID. Get a Video from an ID.
@param _id the video id. It can be a numeric ID, or a page url, or so. :param _id: the video id. It can be a numeric ID, or a page url
@return a Video object :type _id: str
:rtype: :class:`BaseVideo` or None is fot found.
""" """
raise NotImplementedError() raise NotImplementedError()

View file

@ -20,43 +20,89 @@
from datetime import datetime from datetime import datetime
from .base import IBaseCap, CapBaseObject from .base import IBaseCap, CapBaseObject, Field, DateField, FloatField, StringField
__all__ = ['City', 'CityNotFound', 'Current', 'Forecast', 'ICapWeather'] __all__ = ['Forecast', 'Current', 'City', 'CityNotFound', 'ICapWeather']
class Forecast(CapBaseObject): class Forecast(CapBaseObject):
"""
Weather forecast.
"""
date = Field('Date for the forecast', datetime, basestring)
low = FloatField('Low temperature')
high = FloatField('High temperature')
text = StringField('Comment on forecast')
unit = StringField('Unit used for temperatures')
def __init__(self, date, low, high, text, unit): def __init__(self, date, low, high, text, unit):
CapBaseObject.__init__(self, date) CapBaseObject.__init__(self, unicode(date))
self.add_field('date', (basestring,datetime), date) self.date = date
self.add_field('low', (int,float), low) self.low = low
self.add_field('high', (int,float), high) self.high = high
self.add_field('text', basestring, text) self.text = text
self.add_field('unit', basestring, unit) self.unit = unit
class Current(CapBaseObject): class Current(CapBaseObject):
"""
Current weather.
"""
date = DateField('Date of measure')
text = StringField('Comment about current weather')
temp = FloatField('Current temperature')
unit = StringField('Unit used for temperature')
def __init__(self, date, temp, text, unit): def __init__(self, date, temp, text, unit):
CapBaseObject.__init__(self, date) CapBaseObject.__init__(self, unicode(date))
self.add_field('date', (basestring,datetime), date) self.date = date
self.add_field('text', basestring, text) self.text = text
self.add_field('temp', (int,float), temp) self.temp = temp
self.add_field('unit', basestring, unit) self.unit = unit
class City(CapBaseObject): class City(CapBaseObject):
"""
City where to find weather.
"""
name = StringField('Name of city')
def __init__(self, id, name): def __init__(self, id, name):
CapBaseObject.__init__(self, id) CapBaseObject.__init__(self, id)
self.add_field('name', basestring, name) self.name = name
class CityNotFound(Exception): class CityNotFound(Exception):
pass """
Raised when a city is not found.
"""
class ICapWeather(IBaseCap): class ICapWeather(IBaseCap):
"""
Capability for weather websites.
"""
def iter_city_search(self, pattern): def iter_city_search(self, pattern):
"""
Look for a city.
:param pattern: pattern to search
:type pattern: str
:rtype: iter[:class:`City`]
"""
raise NotImplementedError() raise NotImplementedError()
def get_current(self, city_id): def get_current(self, city_id):
"""
Get current weather.
:param city_id: ID of the city
:rtype: :class:`Current`
"""
raise NotImplementedError() raise NotImplementedError()
def iter_forecast(self, city_id): def iter_forecast(self, city_id):
"""
Iter forecasts of a city.
:param city_id: ID of the city
:rtype: iter[:class:`Forecast`]
"""
raise NotImplementedError() raise NotImplementedError()

View file

@ -36,6 +36,19 @@ __all__ = ['Weboob']
class Weboob(object): class Weboob(object):
"""
The main class of Weboob, used to manage backends and call methods.
:param workdir: optional parameter to set path of the working directory
:type workdir: str
:param backends_filename: name of the *backends* file, where configuration of
backends is stored
:type backends_filename: str
:param scheduler: what scheduler to use; default is :class:`weboob.core.scheduler.Scheduler`
:type scheduler: :class:`weboob.core.scheduler.IScheduler`
:param storage: provide a storage where backends can save data
:type storage: :class:`weboob.tools.storage.IStorage`
"""
VERSION = '0.c' VERSION = '0.c'
BACKENDS_FILENAME = 'backends' BACKENDS_FILENAME = 'backends'
@ -64,8 +77,8 @@ class Weboob(object):
if os.path.isdir(old_workdir): if os.path.isdir(old_workdir):
self.logger.warning('You are using "%s" as working directory. Files are moved into %s and %s.' self.logger.warning('You are using "%s" as working directory. Files are moved into %s and %s.'
% (old_workdir, xdg_config_home, xdg_data_home)) % (old_workdir, xdg_config_home, xdg_data_home))
self.create_dir(xdg_config_home) self._create_dir(xdg_config_home)
self.create_dir(xdg_data_home) self._create_dir(xdg_data_home)
for f in os.listdir(old_workdir): for f in os.listdir(old_workdir):
if f in Repositories.SHARE_DIRS: if f in Repositories.SHARE_DIRS:
dest = xdg_data_home dest = xdg_data_home
@ -77,7 +90,7 @@ class Weboob(object):
datadir = xdg_data_home datadir = xdg_data_home
self.workdir = os.path.realpath(workdir) self.workdir = os.path.realpath(workdir)
self.create_dir(workdir) self._create_dir(workdir)
# Repositories management # Repositories management
self.repositories = Repositories(workdir, datadir, self.VERSION) self.repositories = Repositories(workdir, datadir, self.VERSION)
@ -95,7 +108,7 @@ class Weboob(object):
# Storage # Storage
self.storage = storage self.storage = storage
def create_dir(self, name): def _create_dir(self, name):
if not os.path.exists(name): if not os.path.exists(name):
os.makedirs(name) os.makedirs(name)
elif not os.path.isdir(name): elif not os.path.isdir(name):
@ -105,11 +118,15 @@ class Weboob(object):
self.deinit() self.deinit()
def deinit(self): def deinit(self):
"""
Call this method when you stop using Weboob, to
properly unload all correctly.
"""
self.unload_backends() self.unload_backends()
def update(self, progress=IProgress()): def update(self, progress=IProgress()):
""" """
Update modules. Update modules from repositories.
""" """
self.repositories.update(progress) self.repositories.update(progress)
@ -120,13 +137,27 @@ class Weboob(object):
self.repositories.install(minfo, progress) self.repositories.install(minfo, progress)
class LoadError(Exception): class LoadError(Exception):
"""
Raised when a backend is unabled to load.
:param backend_name: name of backend we can't load
:param exception: exception object
"""
def __init__(self, backend_name, exception): def __init__(self, backend_name, exception):
Exception.__init__(self, unicode(exception)) Exception.__init__(self, unicode(exception))
self.backend_name = backend_name self.backend_name = backend_name
def load_backend(self, module_name, params=None, storage=None): def build_backend(self, module_name, params=None, storage=None):
""" """
Load a single backend. Create a single backend which is not listed
in configuration.
:param module_name: name of module
:param params: parameters to give to backend
:type params: :class:`dict`
:param storage: storage to use
:type storage: :class:`weboob.tools.storage.IStorage`
:rtype: :class:`weboob.tools.backend.BaseBackend`
""" """
minfo = self.repositories.get_module_info(module_name) minfo = self.repositories.get_module_info(module_name)
if minfo is None: if minfo is None:
@ -143,14 +174,20 @@ class Weboob(object):
def load_backends(self, caps=None, names=None, modules=None, storage=None, errors=None): def load_backends(self, caps=None, names=None, modules=None, storage=None, errors=None):
""" """
Load backends. Load backends listed in config file.
@param caps [tuple(ICapBase)] load backends which implement all of caps :param caps: load backends which implement all of specified caps
@param names [tuple(unicode)] load backends with instance name in list :type caps: tuple[:class:`weboob.capabilities.base.ICapBase`]
@param modules [tuple(unicode)] load backends which module is in list :param names: load backends with instance name in list
@param storage [IStorage] use the storage if specified :type names: tuple[:class:`str`]
@param errors [list] if specified, store every errors in :param modules: load backends which module is in list
@return [dict(str,BaseBackend)] return loaded backends :type modules: tuple[:class:`str`]
:param storage: use this storage if specified
:type storage: :class:`weboob.tools.storage.IStorage`
:param errors: if specified, store every errors in this list
:type errors: list[:class:`LoadError`]
:returns: loaded backends
:rtype: dict[:class:`str`, :class:`weboob.tools.backend.BaseBackend`]
""" """
loaded = {} loaded = {}
if storage is None: if storage is None:
@ -195,6 +232,12 @@ class Weboob(object):
return loaded return loaded
def unload_backends(self, names=None): def unload_backends(self, names=None):
"""
Unload backends.
:param names: if specified, only unload that backends
:type names: :class:`list`
"""
unloaded = {} unloaded = {}
if isinstance(names, basestring): if isinstance(names, basestring):
names = [names] names = [names]
@ -213,8 +256,11 @@ class Weboob(object):
""" """
Get a backend from its name. Get a backend from its name.
It raises a KeyError if not found. If you set the 'default' parameter, :param name: name of backend to get
the default value is returned instead. :type name: str
:param default: if specified, get this value when the backend is not found
:type default: whatever you want
:raises: :class:`KeyError` if not found.
""" """
try: try:
return self.backend_instances[name] return self.backend_instances[name]
@ -225,6 +271,9 @@ class Weboob(object):
raise raise
def count_backends(self): def count_backends(self):
"""
Get number of loaded backends.
"""
return len(self.backend_instances) return len(self.backend_instances)
def iter_backends(self, caps=None): def iter_backends(self, caps=None):
@ -233,8 +282,9 @@ class Weboob(object):
Note: each backend is locked when it is returned. Note: each backend is locked when it is returned.
@param caps Optional list of capabilities to select backends :param caps: optional list of capabilities to select backends
@return iterator on selected backends. :type caps: tuple[:class:`weboob.capabilities.base.IBaseCap`]
:rtype: iter[:class:`weboob.tools.backend.BaseBackend`]
""" """
for name, backend in sorted(self.backend_instances.iteritems()): for name, backend in sorted(self.backend_instances.iteritems()):
if caps is None or backend.has_caps(caps): if caps is None or backend.has_caps(caps):
@ -248,17 +298,21 @@ class Weboob(object):
This function has two modes: This function has two modes:
- If 'function' is a string, it calls the method with this name on - If *function* is a string, it calls the method with this name on
each backends with the specified arguments; each backends with the specified arguments;
- If 'function' is a callable, it calls it in a separated thread with - If *function* is a callable, it calls it in a separated thread with
the locked backend instance at first arguments, and \*args and the locked backend instance at first arguments, and \*args and
\*\*kwargs. \*\*kwargs.
@param function backend's method name, or callable object :param function: backend's method name, or a callable object
@param backends list of backends to iterate on :type function: :class:`str`
@param caps iterate on backends with this caps :param backends: list of backends to iterate on
@param condition a condition to validate to keep the result :type backends: list[:class:`str`]
@return the BackendsCall object (iterable) :param caps: iterate on backends which implement this caps
:type caps: list[:class:`weboob.capabilities.base.IBaseCap`]
:param condition: a condition to validate results
:type condition: :class:`weboob.core.bcall.IResultsCondition`
:rtype: A :class:`weboob.core.bcall.BackendsCall` object (iterable)
""" """
backends = self.backend_instances.values() backends = self.backend_instances.values()
_backends = kwargs.pop('backends', None) _backends = kwargs.pop('backends', None)
@ -296,16 +350,47 @@ class Weboob(object):
return BackendsCall(backends, condition, function, *args, **kwargs) return BackendsCall(backends, condition, function, *args, **kwargs)
def schedule(self, interval, function, *args): def schedule(self, interval, function, *args):
"""
Schedule an event.
:param interval: delay before calling the function
:type interval: int
:param function: function to call
:type function: callabale
:param args: arguments to give to function
:returns: an event identificator
"""
return self.scheduler.schedule(interval, function, *args) return self.scheduler.schedule(interval, function, *args)
def repeat(self, interval, function, *args): def repeat(self, interval, function, *args):
"""
Repeat a call to a function
:param interval: interval between two calls
:type interval: int
:param function: function to call
:type function: callable
:param args: arguments to give to function
:returns: an event identificator
"""
return self.scheduler.repeat(interval, function, *args) return self.scheduler.repeat(interval, function, *args)
def cancel(self, ev): def cancel(self, ev):
"""
Cancel an event
:param ev: the event identificator
"""
return self.scheduler.cancel(ev) return self.scheduler.cancel(ev)
def want_stop(self): def want_stop(self):
"""
Plan to stop the scheduler.
"""
return self.scheduler.want_stop() return self.scheduler.want_stop()
def loop(self): def loop(self):
"""
Run the scheduler loop
"""
return self.scheduler.run() return self.scheduler.run()

View file

@ -39,7 +39,7 @@ from .formatters.load import FormattersLoader, FormatterLoadError
from .results import ResultsCondition, ResultsConditionError from .results import ResultsCondition, ResultsConditionError
__all__ = ['ReplApplication', 'NotEnoughArguments'] __all__ = ['NotEnoughArguments', 'ReplApplication']
class NotEnoughArguments(Exception): class NotEnoughArguments(Exception):
@ -354,9 +354,10 @@ class ReplApplication(Cmd, ConsoleApplication):
def complete(self, text, state): def complete(self, text, state):
""" """
Override of the Cmd.complete() method to: Override of the Cmd.complete() method to:
- add a space at end of proposals
- display only proposals for words which match the * add a space at end of proposals
text already written by user. * display only proposals for words which match the
text already written by user.
""" """
super(ReplApplication, self).complete(text, state) super(ReplApplication, self).complete(text, state)

View file

@ -28,42 +28,103 @@ from weboob.tools.log import getLogger
from weboob.tools.value import ValuesDict from weboob.tools.value import ValuesDict
__all__ = ['BaseBackend', 'ObjectNotAvailable'] __all__ = ['ObjectNotAvailable', 'BackendStorage', 'BackendConfig', 'BaseBackend']
class ObjectNotAvailable(Exception): class ObjectNotAvailable(Exception):
pass """
Raised when an object is not available.
"""
class BackendStorage(object): class BackendStorage(object):
"""
This is an abstract layer to store data in storages (:mod:`weboob.tools.storage`)
easily.
It is instancied automatically in constructor of :class:`BaseBackend`, in the
:attr:`BaseBackend.storage` attribute.
:param name: name of backend
:param storage: storage object
:type storage: :class:`weboob.tools.storage.IStorage`
"""
def __init__(self, name, storage): def __init__(self, name, storage):
self.name = name self.name = name
self.storage = storage self.storage = storage
def set(self, *args): def set(self, *args):
"""
Set value in the storage.
Example:
>>> backend.storage.set('config', 'nb_of_threads', 10)
>>>
:param args: the path where to store value
"""
if self.storage: if self.storage:
return self.storage.set('backends', self.name, *args) return self.storage.set('backends', self.name, *args)
def delete(self, *args): def delete(self, *args):
"""
Delete a value from the storage.
:param args: path to delete.
"""
if self.storage: if self.storage:
return self.storage.delete('backends', self.name, *args) return self.storage.delete('backends', self.name, *args)
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
"""
Get a value or a dict of values in storage.
Example:
>>> backend.storage.get('config', 'nb_of_threads')
10
>>> backend.storage.get('config', 'unexistant', 'path', default='lol')
'lol'
>>> backend.storage.get('config')
{'nb_of_threads': 10, 'other_things': 'blah'}
:param args: path to get
:param default: if specified, default value when path is not found
"""
if self.storage: if self.storage:
return self.storage.get('backends', self.name, *args, **kwargs) return self.storage.get('backends', self.name, *args, **kwargs)
else: else:
return kwargs.get('default', None) return kwargs.get('default', None)
def load(self, default): def load(self, default):
"""
Load storage.
:param default: this is the default tree if storage is empty
:type default: :class:`dict`
"""
if self.storage: if self.storage:
return self.storage.load('backends', self.name, default) return self.storage.load('backends', self.name, default)
def save(self): def save(self):
"""
Save storage.
"""
if self.storage: if self.storage:
return self.storage.save('backends', self.name) return self.storage.save('backends', self.name)
class BackendConfig(ValuesDict): class BackendConfig(ValuesDict):
"""
Configuration of a backend.
This class is firstly instanced as a :class:`weboob.tools.value.ValuesDict`,
containing some :class:`weboob.tools.value.Value` (and derivated) objects.
Then, using the :func:`load` method will load configuration from file and
create a copy of the :class:`BackendConfig` object with the loaded values.
"""
modname = None modname = None
instname = None instname = None
weboob = None weboob = None
@ -72,12 +133,17 @@ class BackendConfig(ValuesDict):
""" """
Load configuration from dict to create an instance. Load configuration from dict to create an instance.
@param weboob [Weboob] weboob object :param weboob: weboob object
@param modname [str] name of module :type weboob: :class:`weboob.core.ouiboube.Weboob`
@param instname [str] name of instance of this backend :param modname: name of the module
@param params [dict] parameters to load :type modname: :class:`str`
@param nofail [bool] if true, this call can't fail. :param instname: name of this backend
@return [BackendConfig] :type instname: :class:`str`
:param params: parameters to load
:type params: :class:`dict`
:param nofail: if true, this call can't fail
:type nofail: :class:`bool`
:rtype: :class:`BackendConfig`
""" """
cfg = BackendConfig() cfg = BackendConfig()
cfg.modname = modname cfg.modname = modname
@ -103,12 +169,25 @@ class BackendConfig(ValuesDict):
return cfg return cfg
def dump(self): def dump(self):
"""
Dump config in a dictionary.
:rtype: :class:`dict`
"""
settings = {} settings = {}
for name, value in self.iteritems(): for name, value in self.iteritems():
settings[name] = value.dump() settings[name] = value.dump()
return settings return settings
def save(self, edit=True, params=None): def save(self, edit=True, params=None):
"""
Save backend config.
:param edit: if true, it changes config of an existing backend
:type edit: :class:`bool`
:param params: if specified, params to merge with the ones of the current object
:type params: :class:`dict`
"""
assert self.modname is not None assert self.modname is not None
assert self.instname is not None assert self.instname is not None
assert self.weboob is not None assert self.weboob is not None
@ -121,6 +200,22 @@ class BackendConfig(ValuesDict):
class BaseBackend(object): class BaseBackend(object):
"""
Base class for backends.
You may derivate it, and also all capabilities you want to implement.
:param weboob: weboob instance
:type weboob: :class:`weboob.core.ouiboube.Weboob`
:param name: name of backend
:type name: :class:`str`
:param config: configuration of backend
:type config: :class:`dict`
:param storage: storage object
:type storage: :class:`weboob.tools.storage.IStorage`
:param logger: logger
:type logger: :class:`logging.Logger`
"""
# Backend name. # Backend name.
NAME = None NAME = None
# Name of the maintainer of this backend. # Name of the maintainer of this backend.
@ -152,7 +247,9 @@ class BaseBackend(object):
OBJECTS = {} OBJECTS = {}
class ConfigError(Exception): class ConfigError(Exception):
pass """
Raised when the config can't be loaded.
"""
def __enter__(self): def __enter__(self):
self.lock.acquire() self.lock.acquire()
@ -184,16 +281,6 @@ class BaseBackend(object):
""" """
pass pass
class classprop(object):
def __init__(self, fget):
self.fget = fget
def __get__(self, inst, objtype=None):
if inst:
return self.fget(inst)
else:
return self.fget(objtype)
_browser = None _browser = None
@property @property
@ -202,7 +289,7 @@ class BaseBackend(object):
Attribute 'browser'. The browser is created at the first call Attribute 'browser'. The browser is created at the first call
of this attribute, to avoid useless pages access. of this attribute, to avoid useless pages access.
Note that the 'create_default_browser' method is called to create it. Note that the :func:`create_default_browser` method is called to create it.
""" """
if self._browser is None: if self._browser is None:
self._browser = self.create_default_browser() self._browser = self.create_default_browser()
@ -238,6 +325,11 @@ class BaseBackend(object):
@classmethod @classmethod
def iter_caps(klass): def iter_caps(klass):
"""
Iter capabilities implemented by this backend.
:rtype: iter[:class:`weboob.capabilities.base.IBaseCap`]
"""
def iter_caps(cls): def iter_caps(cls):
for base in cls.__bases__: for base in cls.__bases__:
if issubclass(base, IBaseCap) and base != IBaseCap: if issubclass(base, IBaseCap) and base != IBaseCap:
@ -247,6 +339,9 @@ class BaseBackend(object):
return iter_caps(klass) return iter_caps(klass)
def has_caps(self, *caps): def has_caps(self, *caps):
"""
Check if this backend implements at least one of these capabilities.
"""
for c in caps: for c in caps:
if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \ if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \
isinstance(self, c): isinstance(self, c):
@ -255,7 +350,10 @@ class BaseBackend(object):
def fillobj(self, obj, fields=None): def fillobj(self, obj, fields=None):
""" """
@param fields which fields to fill; if None, all fields are filled (list) Fill an object with the wanted fields.
:param fields: what fields to fill; if None, all fields are filled
:type fields: :class:`list`
""" """
def not_loaded(v): def not_loaded(v):
return (v is NotLoaded or isinstance(v, CapBaseObject) and not v.__iscomplete__()) return (v is NotLoaded or isinstance(v, CapBaseObject) and not v.__iscomplete__())

View file

@ -53,7 +53,8 @@ else:
__all__ = ['BrowserIncorrectPassword', 'BrowserBanned', 'BrowserUnavailable', 'BrowserRetry', __all__ = ['BrowserIncorrectPassword', 'BrowserBanned', 'BrowserUnavailable', 'BrowserRetry',
'BrowserHTTPNotFound', 'BrowserHTTPError', 'BasePage', 'BaseBrowser', 'StandardBrowser'] 'BrowserHTTPNotFound', 'BrowserHTTPError', 'BrokenPageError', 'BasePage',
'StandardBrowser', 'BaseBrowser']
# Exceptions # Exceptions
@ -135,6 +136,21 @@ def check_location(func):
return inner return inner
class StandardBrowser(mechanize.Browser): class StandardBrowser(mechanize.Browser):
"""
Standard Browser.
:param firefox_cookies: path to cookies sqlite file
:type firefox_cookies: str
:param parser: parser to use on HTML files
:type parser: :class:`weboob.tools.parsers.iparser.IParser`
:param history: history manager; default value is an object which
does not keep history
:type history: object
:param proxy: proxy URL to use
:type proxy: str
:param factory: mechanize factory. None to use Mechanize's default
:type factory: object
"""
# ------ Class attributes -------------------------------------- # ------ Class attributes --------------------------------------
@ -161,16 +177,6 @@ class StandardBrowser(mechanize.Browser):
default_features.remove('_refresh') default_features.remove('_refresh')
def __init__(self, firefox_cookies=None, parser=None, history=NoHistory(), proxy=None, logger=None, factory=None, responses_dirname=None): def __init__(self, firefox_cookies=None, parser=None, history=NoHistory(), proxy=None, logger=None, factory=None, responses_dirname=None):
"""
Constructor of Browser.
@param filefox_cookies [str] Path to cookies' sqlite file.
@param parser [IParser] parser to use on HTML files.
@param history [object] History manager. Default value is an object
which does not keep history.
@param proxy [str] proxy URL to use.
@param factory [object] Mechanize factory. None to use Mechanize's default.
"""
mechanize.Browser.__init__(self, history=history, factory=factory) mechanize.Browser.__init__(self, history=history, factory=factory)
self.logger = getLogger('browser', logger) self.logger = getLogger('browser', logger)
@ -309,15 +315,16 @@ class StandardBrowser(mechanize.Browser):
def buildurl(base, *args, **kwargs): def buildurl(base, *args, **kwargs):
""" """
Build an URL and escape arguments. Build an URL and escape arguments.
You can give a serie of tuples in *args (and the order is keept), or
a dict in **kwargs (but the order is lost). You can give a serie of tuples in args (and the order is keept), or
a dict in kwargs (but the order is lost).
Example: Example:
>>> buildurl('/blah.php', ('a', '&'), ('b', '=') >>> buildurl('/blah.php', ('a', '&'), ('b', '=')
'/blah.php?a=%26&b=%3D' '/blah.php?a=%26&b=%3D'
>>> buildurl('/blah.php', a='&', 'b'='=') >>> buildurl('/blah.php', a='&', 'b'='=')
'/blah.php?b=%3D&a=%26' '/blah.php?b=%3D&a=%26'
""" """
if not args: if not args:
@ -336,11 +343,16 @@ class StandardBrowser(mechanize.Browser):
""" """
Set a value to a form field. Set a value to a form field.
@param args [dict] arguments where to look for value. :param args: arguments where to look for value
@param label [str] label in args. :type args: dict
@param field [str] field name. If None, use label instead. :param label: label in args
@param value [str] value to give on field. :type label: str
@param is_list [bool] the field is a list. :param field: field name. If None, use label instead
:type field: str
:param value: value to give on field
:type value: str
:param is_list: the field is a list
:type is_list: bool
""" """
try: try:
if not field: if not field:
@ -366,6 +378,29 @@ class StandardBrowser(mechanize.Browser):
class BaseBrowser(StandardBrowser): class BaseBrowser(StandardBrowser):
""" """
Base browser class to navigate on a website. Base browser class to navigate on a website.
:param username: username on website
:type username: str
:param password: password on website. If it is None, Browser will
not try to login
:type password: str
:param firefox_cookies: path to cookies sqlite file
:type firefox_cookies: str
:param parser: parser to use on HTML files
:type parser: :class:`weboob.tools.parsers.iparser.IParser`
:param history: history manager; default value is an object which
does not keep history
:type history: object
:param proxy: proxy URL to use
:type proxy: str
:param logger: logger to use for logging
:type logger: :class:`logging.Logger`
:param factory: mechanize factory. None to use Mechanize's default
:type factory: object
:param get_home: try to get the homepage.
:type get_homme: bool
:param responses_dirname: directory to store responses
:type responses_dirname: str
""" """
# ------ Class attributes -------------------------------------- # ------ Class attributes --------------------------------------
@ -406,20 +441,6 @@ class BaseBrowser(StandardBrowser):
def __init__(self, username=None, password=None, firefox_cookies=None, def __init__(self, username=None, password=None, firefox_cookies=None,
parser=None, history=NoHistory(), proxy=None, logger=None, parser=None, history=NoHistory(), proxy=None, logger=None,
factory=None, get_home=True, responses_dirname=None): factory=None, get_home=True, responses_dirname=None):
"""
Constructor of Browser.
@param username [str] username on website.
@param password [str] password on website. If it is None, Browser will
not try to login.
@param filefox_cookies [str] Path to cookies' sqlite file.
@param parser [IParser] parser to use on HTML files.
@param hisory [object] History manager. Default value is an object
which does not keep history.
@param proxy [str] proxy URL to use.
@param factory [object] Mechanize factory. None to use Mechanize's default.
@param get_home [bool] Try to get the homepage.
"""
StandardBrowser.__init__(self, firefox_cookies, parser, history, proxy, logger, factory, responses_dirname) StandardBrowser.__init__(self, firefox_cookies, parser, history, proxy, logger, factory, responses_dirname)
self.page = None self.page = None
self.last_update = 0.0 self.last_update = 0.0

View file

@ -30,6 +30,9 @@ __all__ = ['FrenchTransaction']
class FrenchTransaction(Transaction): class FrenchTransaction(Transaction):
"""
Transaction with some helpers for french bank websites.
"""
PATTERNS = [] PATTERNS = []
def clean_amount(self, text): def clean_amount(self, text):
@ -61,7 +64,7 @@ class FrenchTransaction(Transaction):
When calling this method, you should have defined patterns (in the When calling this method, you should have defined patterns (in the
PATTERN class attribute) with a list containing tuples of regexp PATTERN class attribute) with a list containing tuples of regexp
and the associated type, for example: and the associated type, for example::
PATTERNS = [(re.compile('^VIR(EMENT)? (?P<text>.*)'), FrenchTransaction.TYPE_TRANSFER), PATTERNS = [(re.compile('^VIR(EMENT)? (?P<text>.*)'), FrenchTransaction.TYPE_TRANSFER),
(re.compile('^PRLV (?P<text>.*)'), FrenchTransaction.TYPE_ORDER), (re.compile('^PRLV (?P<text>.*)'), FrenchTransaction.TYPE_ORDER),
@ -70,8 +73,9 @@ class FrenchTransaction(Transaction):
] ]
In regexps, you can define this patterns: In regexps, you can define this patterns:
- text: part of label to store in simplified label
- yy, mm, dd, HH, MM: date and time parts * text: part of label to store in simplified label
* yy, mm, dd, HH, MM: date and time parts
""" """
if not isinstance(date, (datetime.date, datetime.datetime)): if not isinstance(date, (datetime.date, datetime.datetime)):
if date.isdigit() and len(date) == 8: if date.isdigit() and len(date) == 8:

View file

@ -27,7 +27,9 @@ from weboob.tools.newsfeed import Newsfeed
class GenericNewspaperBackend(BaseBackend, ICapMessages): class GenericNewspaperBackend(BaseBackend, ICapMessages):
"GenericNewspaperBackend class" """
GenericNewspaperBackend class
"""
MAINTAINER = 'Julien Hebert' MAINTAINER = 'Julien Hebert'
EMAIL = 'juke@free.fr' EMAIL = 'juke@free.fr'
VERSION = '0.c' VERSION = '0.c'

View file

@ -17,13 +17,23 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
from weboob.capabilities.base import CapBaseObject, NotLoaded from weboob.capabilities.base import CapBaseObject, NotLoaded, StringField, BytesField
__all__ = ['Thumbnail']
class Thumbnail(CapBaseObject): class Thumbnail(CapBaseObject):
"""
Thumbnail of an image.
"""
url = StringField('URL to photo thumbnail')
data = BytesField('Data')
def __init__(self, url): def __init__(self, url):
CapBaseObject.__init__(self, url) CapBaseObject.__init__(self, url)
self.add_field('url', basestring, url.replace(' ', '%20')) self.url = url.replace(' ', '%20')
self.add_field('data', str)
def __str__(self): def __str__(self):
return self.url return self.url

View file

@ -60,14 +60,18 @@ class LxmlHtmlParser(IParser):
""" """
Select one or many elements from an element, using lxml cssselect by default. Select one or many elements from an element, using lxml cssselect by default.
Raises BrokenPageError if not found. Raises :class:`weboob.tools.browser.browser.BrokenPageError` if not found.
@param element [obj] element on which to apply selector :param element: element on which to apply selector
@param selector [str] CSS or XPath expression :type element: object
@param method [str] (cssselect|xpath) :param selector: CSS or XPath expression
@param nb [int] number of elements expected to be found. :type selector: str
Use None for undefined number, and 'many' for 1 to infinite. :param method: (cssselect|xpath)
@return one or many Element :type method: str
:param nb: number of elements expected to be found. Use None for
undefined number, and 'many' for 1 to infinite
:type nb: :class:`int` or :class:`str`
:rtype: Element
""" """
if method == 'cssselect': if method == 'cssselect':
results = element.cssselect(selector) results = element.cssselect(selector)