diff --git a/docs/source/applications.rst b/docs/source/applications.rst deleted file mode 100644 index 52d84b90..00000000 --- a/docs/source/applications.rst +++ /dev/null @@ -1,5 +0,0 @@ -Applications -============ - -boobank -------- diff --git a/docs/source/backends.rst b/docs/source/backends.rst deleted file mode 100644 index 87ad3a8f..00000000 --- a/docs/source/backends.rst +++ /dev/null @@ -1,5 +0,0 @@ -Backends -======== - -cragr ------ diff --git a/docs/source/genapi.py b/docs/source/genapi.py index 1f912235..53a6c092 100755 --- a/docs/source/genapi.py +++ b/docs/source/genapi.py @@ -10,8 +10,7 @@ def genapi(): os.chdir('api') for root, dirs, files in os.walk('../../../weboob/'): root = root.split('/', 4)[-1] - if root.startswith('applications') or \ - root.startswith('backends'): + if root.startswith('applications'): continue if root.strip(): @@ -26,7 +25,7 @@ def genapi(): continue f, ext = f.rsplit('.', 1) - if ext == 'pyc' or f == '__init__': + if ext != 'py' or f == '__init__': continue subs.add(f) diff --git a/docs/source/index.rst b/docs/source/index.rst index db331271..75d550a2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,18 +3,12 @@ Weboob This is the developer documentation. -.. warning:: - This documentation is being written. - Contents: .. toctree:: :maxdepth: 2 overview - install - backends - applications guides/index api/index diff --git a/docs/source/install.rst b/docs/source/install.rst deleted file mode 100644 index 62fef4f0..00000000 --- a/docs/source/install.rst +++ /dev/null @@ -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`` diff --git a/docs/source/overview.rst b/docs/source/overview.rst index d5a5aabf..10cd353e 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -3,21 +3,13 @@ Overview Weboob (Web Out Of Browsers) provides: -* :doc:`applications` to interact with websites -* :doc:`backends`, each one handles a specific website +* :doc:`applications ` to interact with websites +* :doc:`backends `, each one handles a specific website * a :doc:`core library ` providing all the features needed by backends * :doc:`tools ` to help develop backends and applications 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 ------------ diff --git a/weboob/capabilities/account.py b/weboob/capabilities/account.py index 9638b26d..d1ab9565 100644 --- a/weboob/capabilities/account.py +++ b/weboob/capabilities/account.py @@ -18,23 +18,32 @@ # along with weboob. If not, see . -from .base import IBaseCap, CapBaseObject +from .base import IBaseCap, CapBaseObject, StringField, Field -__all__ = ['ICapAccount'] +__all__ = ['AccountRegisterError', 'Account', 'StatusField', 'ICapAccount'] class AccountRegisterError(Exception): - pass + """ + Raised when there is an error during registration. + """ 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): CapBaseObject.__init__(self, id) - self.add_field('login', basestring) - self.add_field('password', basestring) - self.add_field('properties', dict) class StatusField(object): + """ + Field of an account status. + """ FIELD_TEXT = 0x001 # the value is a long text FIELD_HTML = 0x002 # the value is HTML formated @@ -46,8 +55,14 @@ class StatusField(object): 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 @staticmethod @@ -58,7 +73,9 @@ class ICapAccount(IBaseCap): This is a static method, it would be called even if the backend is 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() @@ -84,6 +101,6 @@ class ICapAccount(IBaseCap): """ Get status of the current account. - @return a list of fields + :returns: a list of fields """ raise NotImplementedError() diff --git a/weboob/capabilities/bank.py b/weboob/capabilities/bank.py index dbbdd5fc..a6681801 100644 --- a/weboob/capabilities/bank.py +++ b/weboob/capabilities/bank.py @@ -18,30 +18,45 @@ # along with weboob. If not, see . -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 -__all__ = ['Account', 'AccountNotFound', 'TransferError', 'ICapBank', 'Transaction'] +__all__ = ['AccountNotFound', 'TransferError', 'Recipient', 'Account', 'Transaction', 'Transfer', 'ICapBank'] class AccountNotFound(Exception): - def __init__(self, msg=None): - if msg is None: - msg = 'Account not found' + """ + Raised when an account is not found. + """ + + def __init__(self, msg='Account not found'): Exception.__init__(self, msg) class TransferError(Exception): - pass + """ + A transfer has failed. + """ class Recipient(CapBaseObject): + """ + Recipient of a transfer. + """ + + label = StringField('Name') + def __init__(self): CapBaseObject.__init__(self, 0) - self.add_field('label', basestring) class Account(Recipient): + """ + Bank account. + + It is a child class of :class:`Recipient`, because an account can be + a recipient of a transfer. + """ TYPE_UNKNOWN = 0 TYPE_CHECKING = 1 # Transaction, everyday transactions TYPE_SAVINGS = 2 # Savings/Deposit, can be used for everyday banking @@ -50,17 +65,18 @@ class Account(Recipient): TYPE_MARKET = 5 # Stock market or other variable investments TYPE_JOINT = 6 # Joint account - def __init__(self): - Recipient.__init__(self) - self.add_field('type', int, self.TYPE_UNKNOWN) - self.add_field('balance', float) - self.add_field('coming', float) + type = IntField('Type of account', default=TYPE_UNKNOWN) + balance = FloatField('Balance on this bank account') + coming = FloatField('Coming balance') def __repr__(self): return u"" % (self.id, self.label) class Transaction(CapBaseObject): + """ + Bank transaction. + """ TYPE_UNKNOWN = 0 TYPE_TRANSFER = 1 TYPE_ORDER = 2 @@ -72,47 +88,77 @@ class Transaction(CapBaseObject): TYPE_LOAN_PAYMENT = 8 TYPE_BANK = 9 - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('date', (basestring, datetime, date)) # debit date - self.add_field('rdate', (datetime, date)) # real date, when the payment has been made - self.add_field('type', int, self.TYPE_UNKNOWN) - self.add_field('raw', unicode) - self.add_field('category', unicode) - self.add_field('label', unicode) - self.add_field('amount', float) + date = DateField('Debit date') + rdate = DateField('Real date, when the payment has been made') + type = IntField('Type of transaction, use TYPE_* constants', default=TYPE_UNKNOWN) + raw = StringField('Raw label of the transaction') + category = StringField('Category of transaction') + label = StringField('Pretty label') + amount = FloatField('Amount of transaction') def __repr__(self): return "" % (self.date, self.label, self.amount) class Transfer(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('amount', float) - self.add_field('date', (basestring, datetime, date)) - self.add_field('origin', (int, long, basestring)) - self.add_field('recipient', (int, long, basestring)) + """ + 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) class ICapBank(ICapCollection): + """ + Capability of bank websites to see accounts and transactions. + """ def iter_resources(self, objs, split_path): + """ + Iter resources. + + Default implementation of this method is to return on top-level + all accounts (by calling :func:`iter_accounts`). + + :param objs: type of objects to get + :type objs: tuple[:class:`CapBaseObject`] + :param split_path: path to discover + :type split_path: :class:`list` + :rtype: iter[:class:`BaseCapObject`] + """ if Account in objs: self._restrict_level(split_path) return self.iter_accounts() def iter_accounts(self): + """ + Iter accounts. + + :rtype: iter[:class:`Account`] + """ raise NotImplementedError() - def get_account(self, _id): + def get_account(self, id): + """ + Get an account from its ID. + + :param id: ID of the account + :type id: :class:`str` + :rtype: :class:`Account` + :raises: :class:`AccountNotFound` + """ raise NotImplementedError() def iter_history(self, account): """ Iter history of transactions on a specific account. - @param account [Account] - @return [iter(Transaction)] + :param account: account to get history + :type account: :class:`Account` + :rtype: iter[:class:`Transaction`] + :raises: :class:`AccountNotFound` """ raise NotImplementedError() @@ -120,8 +166,10 @@ class ICapBank(ICapCollection): """ Iter coming transactions on a specific account. - @param account [Account] - @return [iter(Transaction)] + :param account: account to get coming transactions + :type account: :class:`Account` + :rtype: iter[:class:`Transaction`] + :raises: :class:`AccountNotFound` """ raise NotImplementedError() @@ -129,8 +177,10 @@ class ICapBank(ICapCollection): """ Iter recipients availables for a transfer from a specific account. - @param account [Account] account which initiate the transfer - @return [iter(Recipient)] + :param account: account which initiate the transfer + :type account: :class:`Account` + :rtype: iter[:class:`Recipient`] + :raises: :class:`AccountNotFound` """ raise NotImplementedError() @@ -138,10 +188,15 @@ class ICapBank(ICapCollection): """ Make a transfer from an account to a recipient. - @param account [Account] account to take money - @param recipient [Recipient] account to send money - @param amount [float] amount - @param reason [str] reason of transfer - @return [Transfer] a Transfer object + :param account: account to take money + :type account: :class:`Account` + :param recipient: account to send money + :type recipient: :class:`Recipient` + :param amount: amount + :type amount: :class:`float` + :param reason: reason of transfer + :type reason: :class:`unicode` + :rtype: :class:`Transfer` + :raises: :class:`AccountNotFound`, :class:`TransferError` """ raise NotImplementedError() diff --git a/weboob/capabilities/base.py b/weboob/capabilities/base.py index d0edeabf..95f0966a 100644 --- a/weboob/capabilities/base.py +++ b/weboob/capabilities/base.py @@ -18,14 +18,28 @@ # along with weboob. If not, see . -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', - 'CapBaseObject'] +__all__ = ['FieldNotFound', 'NotAvailable', 'NotLoaded', 'IBaseCap', + 'Field', 'IntField', 'FloatField', 'StringField', 'BytesField', + 'DateField', 'DeltaField', 'CapBaseObject'] 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): Exception.__init__(self, u'Field "%s" not found for object %s' % (field, obj)) @@ -43,6 +57,9 @@ class NotAvailableMeta(type): class NotAvailable(object): + """ + Constant to use on non available fields. + """ __metaclass__ = NotAvailableMeta @@ -58,41 +75,186 @@ class NotLoadedMeta(type): 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 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): - 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): - self.id = id + self.id = to_unicode(id) self.backend = backend + self._fields = deepcopy(self._fields) @property def fullid(self): + """ + Full ID of the object, in form '**ID@backend**'. + """ return '%s@%s' % (self.id, self.backend) - def 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): """ Return True if the object is completed. @@ -111,6 +273,9 @@ class CapBaseObject(object): def set_empty_fields(self, value, excepts=()): """ Set the same value on all empty fields. + + :param value: value to set on all empty fields + :param excepts: if specified, do not change fields listed """ for key, old_value in self.iter_fields(): if old_value in (None, NotLoaded, NotAvailable) and \ @@ -119,22 +284,16 @@ class CapBaseObject(object): 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. - @return [iter(key,value)] iterator on key, value + :rtype: iter[(key, value)] """ - if self.FIELDS is None: - yield 'id', self.id - for key, value in iter_fields(self): - 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) + yield 'id', self.id + for name, field in self._fields.iteritems(): + yield name, field.value def __eq__(self, obj): if isinstance(obj, CapBaseObject): @@ -142,29 +301,32 @@ class CapBaseObject(object): else: return False - class _AttribValue(object): - def __init__(self, type, value): - self.type = type - self.value = value - def __getattr__(self, name): - if self._attribs is not None and name in self._attribs: - return self._attribs[name].value + if self._fields is not None and name in self._fields: + return self._fields[name].value else: raise AttributeError, "'%s' object has no attribute '%s'" % ( self.__class__.__name__, name) def __setattr__(self, name, value): try: - attr = (self._attribs or {})[name] + attr = (self._fields or {})[name] except KeyError: object.__setattr__(self, name, value) 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 NotAvailable and \ value is not None: raise ValueError( 'Value for "%s" needs to be of type %r, not %r' % ( - name, attr.type, type(value))) + name, attr.types, type(value))) attr.value = value diff --git a/weboob/capabilities/bill.py b/weboob/capabilities/bill.py index 98918ebf..ae1044a7 100644 --- a/weboob/capabilities/bill.py +++ b/weboob/capabilities/bill.py @@ -17,74 +17,133 @@ # You should have received a copy of the GNU Affero General Public License # along with weboob. If not, see . -from datetime import datetime, date -from .base import CapBaseObject +from .base import CapBaseObject, StringField, DateField, FloatField from .collection import ICapCollection -__all__ = ['Subscription', 'SubscriptionNotFound', 'ICapBill', 'Detail'] +__all__ = ['SubscriptionNotFound', 'BillNotFound', 'Detail', 'Bill', 'Subscription', 'ICapBill'] class SubscriptionNotFound(Exception): - def __init__(self, msg=None): - if msg is None: - msg = 'Subscription not found' + """ + Raised when a subscription is not found. + """ + def __init__(self, msg='Subscription not found'): Exception.__init__(self, msg) class BillNotFound(Exception): - def __init__(self, msg=None): - if msg is None: - msg = 'Bill not found' + """ + Raised when a bill is not found. + """ + def __init__(self, msg='Bill not found'): Exception.__init__(self, msg) 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): 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): + """ + 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): 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): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('label', basestring) - self.add_field('subscriber', basestring) + """ + Subscription to a service. + """ + label = StringField('label of subscription') + subscriber = StringField('whe has subscribed') class ICapBill(ICapCollection): def iter_resources(self, objs, split_path): + """ + Iter resources. Will return :func:`iter_subscriptions`. + """ if Subscription in objs: self._restrict_level(split_path) - return self.iter_subscription() def iter_subscription(self): + """ + Iter subscriptions. + + :rtype: iter[:class:`Subscription`] + """ raise NotImplementedError() def get_subscription(self, _id): + """ + Get a subscription. + + :param _id: ID of subscription + :rtype: :class:`Subscription` + :raises: :class:`SubscriptionNotFound` + """ raise NotImplementedError() def iter_history(self, subscription): + """ + Iter history of a subscription. + + :param subscription: subscription to get history + :type subscription: :class:`Subscription` + :rtype: iter[:class:`Detail`] + """ raise NotImplementedError() def get_bill(self, id): + """ + Get a bill. + + :param id: ID of bill + :rtype: :class:`Bill` + :raises: :class:`BillNotFound` + """ raise NotImplementedError() def download_bill(self, id): + """ + Download a bill. + + :param id: ID of bill + :rtype: str + :raises: :class:`BillNotFound` + """ raise NotImplementedError() def iter_bills(self, subscription): + """ + Iter bills. + + :param subscription: subscription to get bills + :type subscription: :class:`Subscription` + :rtype: iter[:class:`Bill`] + """ raise NotImplementedError() def get_details(self, subscription): + """ + Get details of a subscription. + + :param subscription: subscription to get bills + :type subscription: :class:`Subscription` + :rtype: iter[:class:`Detail`] + """ raise NotImplementedError() diff --git a/weboob/capabilities/bugtracker.py b/weboob/capabilities/bugtracker.py index f7008207..7a752ba5 100644 --- a/weboob/capabilities/bugtracker.py +++ b/weboob/capabilities/bugtracker.py @@ -17,30 +17,49 @@ # You should have received a copy of the GNU Affero General Public License # along with weboob. If not, see . -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): - pass + """ + Raised when there is an error with an issue. + """ 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): CapBaseObject.__init__(self, id) - self.add_field('name', unicode, name) - self.add_field('members', list) - self.add_field('versions', list) - self.add_field('categories', list) - self.add_field('statuses', list) + self.name = name def __repr__(self): return '' % self.name def find_user(self, id, name): + """ + Find a user from its ID. + + If not found, create a :class:`User` with the specified name. + + :param id: ID of user + :type id: str + :param name: Name of user + :type name: str + :rtype: :class:`User` + """ for user in self.members: if user.id == id: return user @@ -49,6 +68,17 @@ class Project(CapBaseObject): return User(id, name) def find_version(self, id, name): + """ + Find a version from an ID. + + If not found, create a :class:`Version` with the specified name. + + :param id: ID of version + :type id: str + :param name: Name of version + :type name: str + :rtype: :class:`Version` + """ for version in self.versions: if version.id == id: return version @@ -57,6 +87,13 @@ class Project(CapBaseObject): return Version(id, name) def find_status(self, name): + """ + Find a status from a name. + + :param name: Name of status + :type name: str + :rtype: :class:`Status` + """ for status in self.statuses: if status.name == name: return status @@ -65,98 +102,129 @@ class Project(CapBaseObject): return None class User(CapBaseObject): + """ + User. + """ + name = StringField('Name of user') + def __init__(self, id, name): CapBaseObject.__init__(self, id) - self.add_field('name', unicode, name) + self.name = name def __repr__(self): return '' % self.name class Version(CapBaseObject): + """ + Version of a project. + """ + name = StringField('Name of version') + def __init__(self, id, name): CapBaseObject.__init__(self, id) - self.add_field('name', unicode, name) + self.name = name def __repr__(self): return '' % self.name class Status(CapBaseObject): + """ + Status of an issue. + + **VALUE_** constants are the primary status + types. + """ (VALUE_NEW, VALUE_PROGRESS, VALUE_RESOLVED, VALUE_REJECTED) = range(4) + name = StringField('Name of status') + value = IntField('Value of status (constants VALUE_*)') + def __init__(self, id, name, value): CapBaseObject.__init__(self, id) - self.add_field('name', unicode, name) - self.add_field('value', int, value) + self.name = name + self.value = value def __repr__(self): return '' % self.name class Attachment(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('filename', basestring) - self.add_field('url', basestring) + """ + Attachment of an issue. + """ + filename = StringField('Filename') + url = StringField('Direct URL to attachment') def __repr__(self): return '' % self.filename class Change(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('field', unicode) - self.add_field('last', unicode) - self.add_field('new', unicode) + """ + A change of an update. + """ + field = StringField('What field has been changed') + last = StringField('Last value of field') + new = StringField('New value of field') class Update(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('author', User) - self.add_field('date', datetime) - self.add_field('hours', timedelta) - self.add_field('message', unicode) - self.add_field('attachments', (list,tuple)) # Attachment - self.add_field('changes', (list,tuple)) # Change + """ + Represents an update of an issue. + """ + author = Field('Author of update', User) + date = DateField('Date of update') + hours = DeltaField('Time activity') + message = StringField('Log message') + attachments = Field('Files attached to update', list, tuple) + changes = Field('List of changes', list, tuple) def __repr__(self): return '' % self.id class Issue(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('project', Project) - self.add_field('title', unicode) - self.add_field('body', unicode) - self.add_field('creation', datetime) - self.add_field('updated', datetime) - self.add_field('attachments', (list,tuple)) - self.add_field('history', (list,tuple)) - self.add_field('author', User) - self.add_field('assignee', User) - self.add_field('category', unicode) - self.add_field('version', Version) - self.add_field('status', Status) + """ + Represents an issue. + """ + project = Field('Project of this issue', Project) + title = StringField('Title of issue') + body = StringField('Text of issue') + creation = DateField('Date when this issue has been created') + updated = DateField('Date when this issue has been updated for the last time') + attachments = Field('List of attached files', list, tuple) + history = Field('History of updates', list, tuple) + author = Field('Author of this issue', User) + assignee = Field('User assigned to this issue', User) + category = StringField('Name of the category') + version = Field('Target version of this issue', Version) + status = Field('Status of this issue', Status) 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): 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): + """ + Bug trackers websites. + """ def iter_issues(self, query): """ Iter issues with optionnal patterns. - @param query [Query] - @return [iter(Issue)] issues + :param query: query + :type query: :class:`Query` + :rtype: iter[:class:`Issue`] """ raise NotImplementedError() @@ -164,7 +232,8 @@ class ICapBugTracker(IBaseCap): """ Get an issue from its ID. - @return Issue + :param id: ID of issue + :rtype: :class:`Issue` """ raise NotImplementedError() @@ -172,13 +241,19 @@ class ICapBugTracker(IBaseCap): """ 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() def post_issue(self, issue): """ Post an issue to create or update it. + + :param issue: issue to create or update + :type issue: :class:`Issue` """ raise NotImplementedError() @@ -186,14 +261,19 @@ class ICapBugTracker(IBaseCap): """ Add an update to an issue. - @param issue [id,Issue] issue or id of issue - @param update [Update] an Update object + :param issue: issue or id of issue + :type issue: :class:`Issue` + :param update: an Update object + :type update: :class:`Update` """ raise NotImplementedError() def remove_issue(self, issue): """ Remove an issue. + + :param issue: issue + :type issue: :class:`Issue` """ raise NotImplementedError() @@ -201,7 +281,7 @@ class ICapBugTracker(IBaseCap): """ Iter projects. - @return [iter(Project)] projects + :rtype: iter[:class:`Project`] """ raise NotImplementedError() @@ -209,6 +289,6 @@ class ICapBugTracker(IBaseCap): """ Get a project from its ID. - @return [Project] + :rtype: :class:`Project` """ raise NotImplementedError() diff --git a/weboob/capabilities/chat.py b/weboob/capabilities/chat.py index b73c2876..2b81ba20 100644 --- a/weboob/capabilities/chat.py +++ b/weboob/capabilities/chat.py @@ -20,31 +20,60 @@ import datetime -from .base import IBaseCap, CapBaseObject +from .base import IBaseCap, CapBaseObject, StringField, DateField -__all__ = ['ChatException', 'ICapChat'] +__all__ = ['ChatException', 'ChatMessage', 'ICapChat'] class ChatException(Exception): - pass + """ + Exception raised when there is a problem with the chat. + """ 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): CapBaseObject.__init__(self, '%s.%s' % (id_from, id_to)) - self.add_field('id_from', basestring, id_from) - self.add_field('id_to', basestring, id_to) - self.add_field('message', basestring, message) - self.add_field('date', datetime.datetime, date) + self.id_from = id_from + self.id_to = id_to + self.message = message + self.date = date if self.date is None: self.date = datetime.datetime.utcnow() - class ICapChat(IBaseCap): + """ + Websites with a chat system. + """ def iter_chat_messages(self, _id=None): + """ + Iter messages. + + :param _id: optional parameter to only get messages + from a given contact. + :type _id: str + :rtype: iter[:class:`ChatMessage`] + """ raise NotImplementedError() def send_chat_message(self, _id, message): + """ + Send a message to a contact. + + :param _id: ID of recipient + :type _id: str + :param message: message to send + :type message: str + :raises: :class:`ChatException` + """ raise NotImplementedError() diff --git a/weboob/capabilities/contact.py b/weboob/capabilities/contact.py index 90336cb2..6c738558 100644 --- a/weboob/capabilities/contact.py +++ b/weboob/capabilities/contact.py @@ -18,14 +18,17 @@ # along with weboob. If not, see . -from .base import IBaseCap, CapBaseObject +from .base import IBaseCap, CapBaseObject, Field, StringField, BytesField, IntField from weboob.tools.ordereddict import OrderedDict -__all__ = ['ICapContact', 'Contact'] +__all__ = ['ProfileNode', 'ContactPhoto', 'Contact', 'QueryError', 'Query', 'ICapContact'] class ProfileNode(object): + """ + Node of a :class:`Contact` profile. + """ HEAD = 0x01 SECTION = 0x02 @@ -41,14 +44,19 @@ class ProfileNode(object): 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): CapBaseObject.__init__(self, name) - self.add_field('name', basestring, 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) + self.name = name def __iscomplete__(self): return (self.data and (not self.thumbnail_url or self.thumbnail_data)) @@ -63,22 +71,35 @@ class ContactPhoto(CapBaseObject): class Contact(CapBaseObject): + """ + A contact. + """ STATUS_ONLINE = 0x001 STATUS_AWAY = 0x002 STATUS_OFFLINE = 0x004 STATUS_ALL = 0xfff + name = StringField('Name of contact') + status = IntField('Status of contact (STATUS_* constants)') + url = StringField('URL to the profile of contact') + status_msg = StringField('Message of status') + summary = StringField('Description of contact') + photos = Field('List of photos', dict, default=OrderedDict()) + profile = Field('Contact profile', dict) + def __init__(self, id, name, status): CapBaseObject.__init__(self, id) - self.add_field('name', basestring, name) - self.add_field('status', int, 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) + self.name = name + self.status = status def set_photo(self, name, **kwargs): + """ + Set photo of contact. + + :param name: name of photo + :type name: str + :param kwargs: See :class:`ContactPhoto` to know what other parameters you can use + """ if not name in self.photos: self.photos[name] = ContactPhoto(name) @@ -88,13 +109,20 @@ class Contact(CapBaseObject): class QueryError(Exception): - pass + """ + Raised when unable to send a query to a contact. + """ class Query(CapBaseObject): + """ + Query to send to a contact. + """ + message = StringField('Message received') + def __init__(self, id, message): CapBaseObject.__init__(self, id) - self.add_field('message', basestring, message) + self.message = message class ICapContact(IBaseCap): @@ -102,9 +130,11 @@ class ICapContact(IBaseCap): """ Iter contacts - @param status get only contacts with the specified status - @param ids if set, get the specified contacts - @return iterator over the contacts found + :param status: get only contacts with the specified status + :type status: Contact.STATUS_* + :param ids: if set, get the specified contacts + :type ids: list[str] + :rtype: iter[:class:`Contact`] """ raise NotImplementedError() @@ -116,8 +146,9 @@ class ICapContact(IBaseCap): with the proper values, but it might be overloaded by backends. - @param id the ID requested - @return the Contact object, or None if not found + :param id: the ID requested + :type id: str + :rtype: :class:`Contact` or None if not found """ l = self.iter_contacts(ids=[id]) @@ -130,9 +161,10 @@ class ICapContact(IBaseCap): """ Send a query to a contact - @param id the ID of contact - @return a Query object - @except QueryError + :param id: the ID of contact + :type id: str + :rtype: :class:`Query` + :raises: :class:`QueryError` """ raise NotImplementedError() @@ -140,8 +172,9 @@ class ICapContact(IBaseCap): """ Get personal notes about a contact - @param id the ID of the contact - @return a unicode object + :param id: the ID of the contact + :type id: str + :rtype: unicode """ raise NotImplementedError @@ -149,7 +182,8 @@ class ICapContact(IBaseCap): """ Set personal notes about a contact - @param id the ID of the contact - @param notes the unicode object to save as notes + :param id: the ID of the contact + :type id: str + :returns: the unicode object to save as notes """ raise NotImplementedError diff --git a/weboob/capabilities/content.py b/weboob/capabilities/content.py index ff816d18..8d528e0f 100644 --- a/weboob/capabilities/content.py +++ b/weboob/capabilities/content.py @@ -18,37 +18,74 @@ # along with weboob. If not, see . -from .base import IBaseCap, CapBaseObject -from datetime import datetime +from .base import IBaseCap, CapBaseObject, StringField, DateField, Field + + +__all__ = ['Content', 'Revision', 'ICapContent'] + class Content(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('title', basestring) - self.add_field('author', basestring) - self.add_field('content', basestring) - self.add_field('revision', basestring) + """ + Content object. + """ + title = StringField('Title of content') + author = StringField('Original author of content') + content = StringField('Body') + revision = StringField('ID of revision') class Revision(CapBaseObject): - def __init__(self, _id): - CapBaseObject.__init__(self, _id) - self.add_field('author', basestring) - self.add_field('comment', basestring) - self.add_field('revision', basestring) - self.add_field('timestamp', datetime) - self.add_field('minor', bool) - - + """ + Revision of a change on a content. + """ + author = StringField('Author of revision') + comment = StringField('Comment log about revision') + timestamp = DateField('Date of revision') + minor = Field('Is this change minor?', bool) class ICapContent(IBaseCap): def get_content(self, id, revision=None): + """ + Get a content from an ID. + + :param id: ID of content + :type id: str + :param revision: if given, get the content at this revision + :type revision: :class:`Revision` + :rtype: :class:`Content` + """ raise NotImplementedError() def iter_revisions(self, id, 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() def push_content(self, content, message=None, minor=False): + """ + Push a new revision of a content. + + :param content: object to push + :type content: :class:`Content` + :param message: log message to associate to new revision + :type message: str + :param minor: this is a minor revision + :type minor: bool + """ raise NotImplementedError() def get_content_preview(self, content): + """ + Get a HTML preview of a content. + + :param content: content object + :type content: :class:`Content` + :rtype: str + """ raise NotImplementedError() diff --git a/weboob/capabilities/dating.py b/weboob/capabilities/dating.py index fd8c8d9a..895f6f73 100644 --- a/weboob/capabilities/dating.py +++ b/weboob/capabilities/dating.py @@ -18,55 +18,103 @@ # along with weboob. If not, see . -import datetime - -from .base import IBaseCap, CapBaseObject +from .base import IBaseCap, CapBaseObject, Field, StringField, DateField from .contact import Contact -__all__ = ['ICapDating'] +__all__ = ['OptimizationNotFound', 'Optimization', 'Event', 'ICapDating'] class OptimizationNotFound(Exception): - pass + """ + Raised when an optimization is not found. + """ 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 = {} def start(self): + """ + Start optimization. + """ raise NotImplementedError() def stop(self): + """ + Stop optimization. + """ raise NotImplementedError() def is_running(self): + """ + Know if the optimization is currently running. + + :rtype: bool + """ raise NotImplementedError() def get_config(self): + """ + Get config of this optimization. + + :rtype: dict + """ return None def set_config(self, params): + """ + Set config of this optimization. + + :param params: parameters + :type params: dict + """ raise NotImplementedError() class Event(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('date', (datetime.datetime)) - self.add_field('contact', Contact) - self.add_field('type', basestring) - self.add_field('message', basestring) + """ + A dating event (for example a visite, a query received, etc.) + """ + date = DateField('Date of event') + contact = Field('Contact related to this event', Contact) + type = StringField('Type of event') + message = StringField('Message of the event') class ICapDating(IBaseCap): + """ + Capability for dating websites. + """ def init_optimizations(self): + """ + Initialization of optimizations. + """ raise NotImplementedError() def add_optimization(self, name, optim): + """ + Add an optimization. + + :param name: name of optimization + :type name: str + :param optim: optimization + :type optim: :class:`Optimization` + """ setattr(self, 'OPTIM_%s' % name, optim) - def iter_optimizations(self, *optims): + def iter_optimizations(self): + """ + Iter optimizations. + + :rtype: iter[:class:`Optimization`] + """ for attr_name in dir(self): if not attr_name.startswith('OPTIM_'): continue @@ -77,6 +125,13 @@ class ICapDating(IBaseCap): yield attr_name[6:], attr def get_optimization(self, optim): + """ + Get an optimization from a name. + + :param optim: name of optimization + :type optim: str + :rtype: :class:`Optimization` + """ optim = optim.upper() if not hasattr(self, 'OPTIM_%s' % optim): raise OptimizationNotFound() @@ -84,4 +139,9 @@ class ICapDating(IBaseCap): return getattr(self, 'OPTIM_%s' % optim) def iter_events(self): + """ + Iter events. + + :rtype: iter[:class:`Event`] + """ raise NotImplementedError() diff --git a/weboob/capabilities/gallery.py b/weboob/capabilities/gallery.py index 1f3e50b0..988c1468 100644 --- a/weboob/capabilities/gallery.py +++ b/weboob/capabilities/gallery.py @@ -17,31 +17,39 @@ # You should have received a copy of the GNU Affero General Public License # along with weboob. If not, see . -from datetime import datetime from weboob.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): """ Represents a gallery. + This object has to be inherited to specify how to calculate the URL of the gallery from its ID. """ + title = StringField('Title of gallery') + url = StringField('Direct URL to gallery') + description = StringField('Description of gallery') + cardinality = IntField('Cardinality of gallery') + date = DateField('Date of gallery') + rating = FloatField('Rating of this gallery') + rating_max = FloatField('Max rating available') + thumbnail = Field('Thumbnail', Thumbnail) + def __init__(self, _id, title=NotLoaded, url=NotLoaded, cardinality=NotLoaded, date=NotLoaded, rating=NotLoaded, rating_max=NotLoaded, thumbnail=NotLoaded, thumbnail_url=None, nsfw=False): CapBaseObject.__init__(self, unicode(_id)) - self.add_field('title', basestring, title) - self.add_field('url', basestring, url) - self.add_field('description', basestring) - self.add_field('cardinality', int) - self.add_field('date', datetime, date) - self.add_field('rating', (int, long, float), rating) - self.add_field('rating_max', (int, long, float), rating_max) - self.add_field('thumbnail', Thumbnail, thumbnail) + self.title = title + self.url = url + self.date = date + self.rating = rating + self.rating_max = rating_max + self.thumbnail = thumbnail @classmethod def id2url(cls, _id): @@ -50,24 +58,39 @@ class BaseGallery(CapBaseObject): @property def page_url(self): + """ + Get URL to page of this gallery. + """ return self.id2url(self.id) def iter_image(self): + """ + Iter images. + """ raise NotImplementedError() class BaseImage(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, ext=NotLoaded, gallery=None): CapBaseObject.__init__(self, unicode(_id)) - self.add_field('index', int, index) # usually page number - self.add_field('thumbnail', Thumbnail, thumbnail) - self.add_field('url', basestring, url) - self.add_field('ext', basestring, ext) - self.add_field('data', str) - self.add_field('gallery', BaseGallery, gallery) + self.index = index + self.thumbnail = thumbnail + self.url = url + self.ext = ext + self.gallery = gallery def __str__(self): return self.url @@ -92,10 +115,15 @@ class ICapGallery(IBaseCap): """ Iter results of a search on a pattern. - @param pattern [str] pattern to search on - @param sortby [enum] sort by... - @param nsfw [bool] include non-suitable for work videos if True - @param max_results [int] maximum number of results to return + :param pattern: pattern to search on + :type pattern: str + :param sortby: sort by... + :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() @@ -103,7 +131,8 @@ class ICapGallery(IBaseCap): """ Get gallery from an ID. - @param _id the gallery id. It can be a numeric ID, or a page url, or so. - @return a Gallery object + :param _id: the gallery id. It can be a numeric ID, or a page url, or so. + :type _id: str + :rtype: :class:`Gallery` """ raise NotImplementedError() diff --git a/weboob/capabilities/gauge.py b/weboob/capabilities/gauge.py index 886de90b..775d48bb 100644 --- a/weboob/capabilities/gauge.py +++ b/weboob/capabilities/gauge.py @@ -16,39 +16,42 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -from datetime import datetime -from .base import IBaseCap, CapBaseObject +from .base import IBaseCap, CapBaseObject, StringField, FloatField, DateField -__all__ = ['ICapWaterLevel'] +__all__ = ['Gauge', 'GaugeMeasure', 'ICapWaterLevel'] class Gauge(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - - self.add_field('name', basestring) - self.add_field('river', basestring) - self.add_field('level', float) - self.add_field('flow', float) - self.add_field('lastdate', datetime) - self.add_field('forecast', basestring) + """ + Gauge class. + """ + name = StringField('Name of gauge') + river = StringField('What river') + level = FloatField('Level of gauge') + flow = FloatField('Flow of gauge') + lastdate = DateField('Last measure') + forecast = StringField('Forecast') 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): CapBaseObject.__init__(self, None) - self.add_field('level', float) - self.add_field('flow', float) - self.add_field('date', datetime) - class ICapWaterLevel(IBaseCap): def iter_gauge_history(self, id): """ Get history of a gauge. - @param id [str] ID of the river - @return [iter(GaugeMeasure)] + :param id: ID of the river + :type id: str + :rtype: iter[:class:`GaugeMeasure`] """ raise NotImplementedError() @@ -56,8 +59,9 @@ class ICapWaterLevel(IBaseCap): """ Get last measure of the gauge. - @param id [str] ID of the gauge. - @return [GaugeMeasure] + :param id: ID of the gauge + :type id: str + :rtype: :class:`GaugeMeasure` """ raise NotImplementedError() @@ -65,7 +69,8 @@ class ICapWaterLevel(IBaseCap): """ Iter gauges. - @param pattern [str] if specified, used to search gauges - @return [iter(Gauge)] + :param pattern: if specified, used to search gauges + :type pattern: str + :rtype: iter[:class:`Gauge`] """ raise NotImplementedError() diff --git a/weboob/capabilities/geolocip.py b/weboob/capabilities/geolocip.py index 4e09bf6d..6a071a9c 100644 --- a/weboob/capabilities/geolocip.py +++ b/weboob/capabilities/geolocip.py @@ -18,27 +18,40 @@ # along with weboob. If not, see . -from .base import IBaseCap, CapBaseObject +from .base import IBaseCap, CapBaseObject, StringField, FloatField __all__ = ['IpLocation', 'ICapGeolocIp'] 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): CapBaseObject.__init__(self, 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): + """ + Access information about IP addresses database. + """ def get_location(self, ipaddr): + """ + Get location of an IP address. + + :param ipaddr: IP address + :type ipaddr: str + :rtype: :class:`IpLocation` + """ raise NotImplementedError() diff --git a/weboob/capabilities/housing.py b/weboob/capabilities/housing.py index 46ebd490..306f8fe9 100644 --- a/weboob/capabilities/housing.py +++ b/weboob/capabilities/housing.py @@ -18,19 +18,23 @@ # along with weboob. If not, see . -from datetime import date - -from .base import IBaseCap, CapBaseObject +from .base import IBaseCap, CapBaseObject, Field, IntField, FloatField, \ + StringField, BytesField, DateField -__all__ = ['ICapHousing'] +__all__ = ['HousingPhoto', 'Housing', 'Query', 'City', 'ICapHousing'] class HousingPhoto(CapBaseObject): + """ + Photo of a housing. + """ + url = StringField('Direct URL to photo') + data = BytesField('Data of photo') + def __init__(self, url): CapBaseObject.__init__(self, url.split('/')[-1]) - self.add_field('url', basestring, url) - self.add_field('data', str) + self.url = url def __iscomplete__(self): return self.data @@ -42,45 +46,75 @@ class HousingPhoto(CapBaseObject): return u'' % (self.id, len(self.data) if self.data else 0) class Housing(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('title', basestring) - self.add_field('area', (int,float)) - self.add_field('cost', (int,float)) - self.add_field('currency', basestring) - self.add_field('date', date) - self.add_field('location', basestring) - self.add_field('station', basestring) - self.add_field('text', basestring) - self.add_field('phone', basestring) - self.add_field('photos', list) - self.add_field('details', dict) + """ + Content of a housing. + """ + title = StringField('Title of housing') + area = FloatField('Area of housing, in m2') + cost = FloatField('Cost of housing') + currency = StringField('Currency of cost') + date = DateField('Date when the housing has been published') + location = StringField('Location of housing') + station = StringField('What metro/bus station next to housing') + text = StringField('Text of the housing') + phone = StringField('Phone number to contact') + photos = Field('List of photos', list) + details = Field('Key/values of details', dict) class Query(CapBaseObject): + """ + Query to find housings. + """ TYPE_RENT = 0 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): 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): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('name', basestring) + """ + City. + """ + name = StringField('Name of city') class ICapHousing(IBaseCap): + """ + Capability of websites to search housings. + """ def search_housings(self, query): + """ + Search housings. + + :param query: search query + :type query: :class:`Query` + :rtype: iter[:class:`Housing`] + """ raise NotImplementedError() def get_housing(self, housing): + """ + Get an housing from an ID. + + :param housing: ID of the housing + :type housing: str + :rtype: :class:`Housing` or None if not found. + """ raise NotImplementedError() def search_city(self, pattern): + """ + Search a city from a pattern. + + :param pattern: pattern to search + :type pattern: str + :rtype: iter[:class:`City`] + """ raise NotImplementedError() diff --git a/weboob/capabilities/library.py b/weboob/capabilities/library.py index 594fbdef..ebe0169f 100644 --- a/weboob/capabilities/library.py +++ b/weboob/capabilities/library.py @@ -17,40 +17,59 @@ # You should have received a copy of the GNU Affero General Public License # along with weboob. If not, see . -from datetime import datetime, date - from .collection import ICapCollection -from .base import CapBaseObject +from .base import CapBaseObject, Field, StringField, DateField -__all__ = ['ICapBook', 'Book'] +__all__ = ['Book', 'Renew', 'ICapBook'] class Book(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('name', basestring) - self.add_field('author', basestring) - self.add_field('location', basestring) - self.add_field('date', (datetime, date)) # which may be the due date - self.add_field('late', bool) + """ + Describes a book. + """ + name = StringField('Name of the book') + author = StringField('Author of the book') + location = StringField('Location') + date = DateField('The due date') + late = Field('Are you late?', bool) class Renew(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('message', basestring) + """ + A renew message. + """ + message = StringField('Message') class ICapBook(ICapCollection): + """ + Library websites. + """ def iter_resources(self, objs, split_path): + """ + Iter resources. It retuns :func:`iter_books`. + """ if Book in objs: self._restrict_level(split_path) - return self.iter_books() def iter_books(self, pattern): + """ + Iter books from a pattern. + + :param pattern: pattern to search + :type pattern: str + :rtype: iter[:class:`Book`] + """ raise NotImplementedError() def get_book(self, _id): + """ + Get a book from an ID. + + :param _id: ID of the book + :type _id: str + :rtype: :class:`Book` + """ raise NotImplementedError() def get_booked(self, _id): diff --git a/weboob/capabilities/messages.py b/weboob/capabilities/messages.py index acb5e3ab..292977f0 100644 --- a/weboob/capabilities/messages.py +++ b/weboob/capabilities/messages.py @@ -21,18 +21,42 @@ import datetime 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_UNREAD = 0x002 # The message is unread IS_RECEIVED = 0x004 # The receiver has 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, title=NotLoaded, sender=NotLoaded, @@ -45,16 +69,14 @@ class Message(CapBaseObject): flags=0): CapBaseObject.__init__(self, id) assert thread is not None - self.add_field('thread', Thread, thread) - self.add_field('title', basestring, title) - self.add_field('sender', basestring, sender) - self.add_field('receivers', list, receivers) - self.add_field('date', (datetime.datetime, datetime.date), date) - self.add_field('parent', Message, parent) - self.add_field('content', basestring, content) - self.add_field('signature', basestring, signature) - self.add_field('children', list, children) - self.add_field('flags', int, flags) + self.thread = thread + self.title = title + self.sender = sender + self.receivers = receivers + self.content = content + self.signature = signature + self.children = children + self.flags = flags if date is None: date = datetime.datetime.utcnow() @@ -68,14 +90,23 @@ class Message(CapBaseObject): @property def date_int(self): + """ + Date of message as an integer. + """ return int(time.strftime('%Y%m%d%H%M%S', self.date.timetuple())) @property def full_id(self): + """ + Full ID of message (in form '**THREAD_ID.MESSAGE_ID**') + """ return '%s.%s' % (self.thread.id, self.id) @property def full_parent_id(self): + """ + Get the full ID of the parent message (in form '**THREAD_ID.MESSAGE_ID**'). + """ if self.parent: return self.parent.full_id elif self._parent_id is None: @@ -96,18 +127,24 @@ class Message(CapBaseObject): return '' % ( self.full_id, self.title, self.date, self.sender) -class Thread(CapBaseObject): +class Thread(_Thread): + """ + Thread containing messages. + """ IS_THREADS = 0x001 IS_DISCUSSION = 0x002 - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('root', Message) - self.add_field('title', basestring) - self.add_field('date', (datetime.datetime, datetime.date)) - self.add_field('flags', int, self.IS_THREADS) + root = Field('Root message', Message) + title = StringField('Title of thread') + date = DateField('Date of thread') + flags = IntField('Flags (IS_* constants)', default=0) def iter_all_messages(self): + """ + Iter all messages of the thread. + + :rtype: iter[:class:`Message`] + """ if self.root: yield self.root for m in self._iter_all_messages(self.root): @@ -122,11 +159,14 @@ class Thread(CapBaseObject): class ICapMessages(IBaseCap): + """ + Capability to read messages. + """ def iter_threads(self): """ Iterates on threads, from newers to olders. - @return [iter] Thread objects + :rtype: iter[:class:`Thread`] """ raise NotImplementedError() @@ -134,7 +174,7 @@ class ICapMessages(IBaseCap): """ Get a specific thread. - @return [Thread] the Thread object + :rtype: :class:`Thread` """ raise NotImplementedError() @@ -142,7 +182,7 @@ class ICapMessages(IBaseCap): """ Iterates on messages which hasn't been marked as read. - @return [iter] Message objects + :rtype: iter[:class:`Message`] """ raise NotImplementedError() @@ -150,19 +190,26 @@ class ICapMessages(IBaseCap): """ 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() class CantSendMessage(Exception): - pass + """ + Raised when a message can't be send. + """ class ICapMessagesPost(IBaseCap): + """ + This capability allow user to send a message. + """ def post_message(self, message): """ Post a message. - @param message Message object - @return + :param message: message to send + :type message: :class:`Message` + :raises: :class:`CantSendMessage` """ raise NotImplementedError() diff --git a/weboob/capabilities/paste.py b/weboob/capabilities/paste.py index 5e555652..4bdb352f 100644 --- a/weboob/capabilities/paste.py +++ b/weboob/capabilities/paste.py @@ -18,27 +18,34 @@ # along with weboob. If not, see . -from .base import IBaseCap, CapBaseObject, NotLoaded +from .base import IBaseCap, CapBaseObject, NotLoaded, Field, StringField __all__ = ['PasteNotFound', 'BasePaste', 'ICapPaste'] class PasteNotFound(Exception): - pass + """ + Raised when a paste is not found. + """ class BasePaste(CapBaseObject): """ Represents a pasted text. """ + title = StringField('Title of paste') + language = StringField('Language of the paste') + contents = StringField('Content of the paste') + public = Field('Is this paste public?', bool) + def __init__(self, _id, title=NotLoaded, language=NotLoaded, contents=NotLoaded, public=NotLoaded): CapBaseObject.__init__(self, unicode(_id)) - self.add_field('title', basestring, title) - self.add_field('language', basestring, language) - self.add_field('contents', basestring, contents) - self.add_field('public', bool, public) + self.title = title + self.language = language + self.contents = contents + self.public = public @classmethod def id2url(cls, _id): @@ -47,6 +54,9 @@ class BasePaste(CapBaseObject): @property def page_url(self): + """ + Get URL to page of this paste. + """ return self.id2url(self.id) @@ -60,7 +70,7 @@ class ICapPaste(IBaseCap): Get a new paste object for posting it with the backend. The parameters should be passed to the object init. - @return a Paste object + :rtype: :class:`BasePaste` """ raise NotImplementedError() @@ -79,7 +89,8 @@ class ICapPaste(IBaseCap): A score of 1 means the backend is suitable. Higher scores means it is more suitable than others with a lower score. - @return int Score + :rtype: int + :returns: score """ raise NotImplementedError() @@ -87,8 +98,10 @@ class ICapPaste(IBaseCap): """ Get a Paste from an ID or URL. - @param _id the paste id. It can be an ID or a page URL. - @return a Paste object + :param _id: the paste id. It can be an ID or a page URL. + :type _id: str + :rtype: :class:`BasePaste` + :raises: :class:`PasteNotFound` """ raise NotImplementedError() @@ -96,7 +109,7 @@ class ICapPaste(IBaseCap): """ Post a paste. - @param paste Paste object - @return + :param paste: a Paste object + :type paste: :class:`BasePaste` """ raise NotImplementedError() diff --git a/weboob/capabilities/radio.py b/weboob/capabilities/radio.py index b755fce9..daaa41fc 100644 --- a/weboob/capabilities/radio.py +++ b/weboob/capabilities/radio.py @@ -18,17 +18,18 @@ # along with weboob. If not, see . -from .base import IBaseCap, CapBaseObject +from .base import IBaseCap, CapBaseObject, Field, StringField __all__ = ['Emission', 'Stream', 'Radio', 'ICapRadio'] class Emission(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('artist', unicode) - self.add_field('title', unicode) + """ + Emission of a radio. + """ + artist = StringField('Name of artist') + title = StringField('Title of song or emission') def __iscomplete__(self): # This volatile information may be reloaded everytimes. @@ -41,10 +42,11 @@ class Emission(CapBaseObject): return self.title class Stream(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('title', unicode) - self.add_field('url', unicode) + """ + Stream of a radio. + """ + title = StringField('Title of stream') + url = StringField('Direct URL to the stream') def __unicode__(self): return u'%s (%s)' % (self.title, self.url) @@ -53,16 +55,34 @@ class Stream(CapBaseObject): return self.__unicode__() class Radio(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - self.add_field('title', unicode) - self.add_field('description', unicode) - self.add_field('current', Emission) - self.add_field('streams', list) + """ + Radio object. + """ + title = StringField('Title of radio') + description = StringField('Description of radio') + current = Field('Current emission', Emission) + streams = Field('List of streams', list) class ICapRadio(IBaseCap): + """ + Capability of radio websites. + """ def iter_radios_search(self, pattern): + """ + Search a radio. + + :param pattern: pattern to search + :type pattern: str + :rtype: iter[:class:`Radio`] + """ raise NotImplementedError() def get_radio(self, id): + """ + Get a radio from an ID. + + :param id: ID of radio + :type id: str + :rtype: :class:`Radio` + """ raise NotImplementedError() diff --git a/weboob/capabilities/torrent.py b/weboob/capabilities/torrent.py index e53271ca..7f128aec 100644 --- a/weboob/capabilities/torrent.py +++ b/weboob/capabilities/torrent.py @@ -17,41 +17,72 @@ # You should have received a copy of the GNU Affero General Public License # along with weboob. If not, see . -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): + """ + Raised when trying to get URL to torrent but only magnet is available. + """ def __init__(self, magnet): self.magnet = magnet Exception.__init__(self, 'Only magnet URL is available') 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): CapBaseObject.__init__(self, id) - self.add_field('name', basestring, 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 + self.name = name class ICapTorrent(IBaseCap): + """ + Torrent trackers. + """ def iter_torrents(self, pattern): + """ + Search torrents and iterate on results. + + :param pattern: pattern to search + :type pattern: str + :rtype: iter[:class:`Torrent`] + """ raise NotImplementedError() def get_torrent(self, _id): + """ + Get a torrent object from an ID. + + :param _id: ID of torrent + :type _id: str + :rtype: :class:`Torrent` + """ raise NotImplementedError() def get_torrent_file(self, _id): + """ + Get the content of the .torrent file. + + :param _id: ID of torrent + :type _id: str + :rtype: str + """ raise NotImplementedError() diff --git a/weboob/capabilities/travel.py b/weboob/capabilities/travel.py index b46186dc..5c16822b 100644 --- a/weboob/capabilities/travel.py +++ b/weboob/capabilities/travel.py @@ -18,76 +18,98 @@ # along with weboob. If not, see . -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): + """ + Describes a station. + """ + name = StringField('Name of station') + def __init__(self, id, name): CapBaseObject.__init__(self, id) - self.add_field('name', basestring, name) + self.name = name def __repr__(self): return "" % (self.id, self.name) 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): CapBaseObject.__init__(self, id) - self.add_field('type', basestring, _type) - self.add_field('time', datetime, _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) + self.type = _type + self.time = _time def __repr__(self): return u"" % ( self.id, self.type, self.time.strftime('%H:%M'), self.departure_station, self.arrival_station) class RoadStep(CapBaseObject): - def __init__(self, id): - CapBaseObject.__init__(self, id) - - self.add_field('line', basestring) - self.add_field('start_time', time) - self.add_field('end_time', time) - self.add_field('departure', unicode) - self.add_field('arrival', unicode) - self.add_field('duration', timedelta) + """ + A step on a roadmap. + """ + line = StringField('When line') + start_time = TimeField('Start of step') + end_time = TimeField('End of step') + departure = StringField('Departure station') + arrival = StringField('Arrival station') + duration = DeltaField('Duration of this step') class RoadmapError(Exception): - pass + """ + Raised when the roadmap is unable to be calculated. + """ class RoadmapFilters(CapBaseObject): + """ + Filters to get a roadmap. + """ + departure_time = DateField('Wanted departure time') + arrival_time = DateField('Wanted arrival time') + def __init__(self): CapBaseObject.__init__(self, '') - self.add_field('departure_time', datetime) - self.add_field('arrival_time', datetime) - class ICapTravel(IBaseCap): + """ + Travel websites. + """ def iter_station_search(self, pattern): """ Iterates on search results of stations. - @param pattern [str] the search pattern - @return [iter] the of Station objects + :param pattern: the search pattern + :type pattern: str + :rtype: iter[:class:`Station`] """ raise NotImplementedError() - def iter_station_departures(self, station_id, arrival_id): + def iter_station_departures(self, station_id, arrival_id=None): """ Iterate on departures. - @param station_id [id] the station id - @param arrival_id [id] optionnal arrival station id - @return [iter] result of Departure objects + :param station_id: the station ID + :type station_id: str + :param arrival_id: optionnal arrival station ID + :type arrival_id: str + :rtype: iter[:class:`Departure`] """ raise NotImplementedError() @@ -95,9 +117,12 @@ class ICapTravel(IBaseCap): """ Get a roadmap. - @param departure [str] name of departure station - @param arrival [str] name of arrival station - @param filters [RoadmapFilters] filters on search - @return [iter(RoadStep)] steps of roadmap + :param departure: name of departure station + :type departure: str + :param arrival: name of arrival station + :type arrival: str + :param filters: filters on search + :type filters: :class:`RoadmapFilters` + :rtype: iter[:class:`RoadStep`] """ raise NotImplementedError() diff --git a/weboob/capabilities/video.py b/weboob/capabilities/video.py index 6359b8ea..ca8b1530 100644 --- a/weboob/capabilities/video.py +++ b/weboob/capabilities/video.py @@ -1,6 +1,6 @@ # -*- 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. # @@ -18,34 +18,33 @@ # along with weboob. If not, see . -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 __all__ = ['BaseVideo', 'ICapVideo'] + class BaseVideo(CapBaseObject): """ Represents a video. + This object has to be inherited to specify how to calculate the URL of the video from its ID. """ - def __init__(self, _id): - CapBaseObject.__init__(self, unicode(_id)) - - self.add_field('title', basestring) - self.add_field('url', basestring) - self.add_field('ext', basestring) - self.add_field('author', basestring) - self.add_field('description', basestring) - self.add_field('duration', (int,long,timedelta)) - self.add_field('date', datetime) - self.add_field('rating', (int,long,float), NotAvailable) - self.add_field('rating_max', (int,long,float), NotAvailable) - self.add_field('thumbnail', Thumbnail) - self.add_field('nsfw', bool, False) + title = StringField('Title of video') + url = StringField('URL to the video file') + ext = StringField('Extension of video') + author = StringField('Author of video') + description = StringField('Description of video') + duration = Field('Duration of video', int, long, timedelta) + date = DateField('Date when the video has been published') + rating = Field('Rating of video', int, long, float, default=NotAvailable) + rating_max = Field('Max rating', int, long, float, default=NotAvailable) + thumbnail = Field('Thumbnail of video', Thumbnail) + nsfw = Field('Is this video Not Safe For Work', bool, default=False) @classmethod def id2url(cls, _id): @@ -54,6 +53,9 @@ class BaseVideo(CapBaseObject): @property def page_url(self): + """ + Get page URL of the video. + """ return self.id2url(self.id) @@ -70,10 +72,14 @@ class ICapVideo(IBaseCap): """ Iter results of a search on a pattern. - @param pattern [str] pattern to search on - @param sortby [enum] sort by... - @param nsfw [bool] include non-suitable for work videos if True - @param max_results [int] maximum number of results to return + :param pattern: pattern to search on + :type pattern: str + :param sortby: sort by... (use SEARCH_* constants) + :param nsfw: include non-suitable for work videos if True + :type nsfw: bool + :param max_results: maximum number of results to return + :type max_results: int + :rtype: iter[:class:`BaseVideo`] """ raise NotImplementedError() @@ -81,7 +87,8 @@ class ICapVideo(IBaseCap): """ Get a Video from an ID. - @param _id the video id. It can be a numeric ID, or a page url, or so. - @return a Video object + :param _id: the video id. It can be a numeric ID, or a page url + :type _id: str + :rtype: :class:`BaseVideo` or None is fot found. """ raise NotImplementedError() diff --git a/weboob/capabilities/weather.py b/weboob/capabilities/weather.py index e16b78ee..4213dc2e 100644 --- a/weboob/capabilities/weather.py +++ b/weboob/capabilities/weather.py @@ -20,43 +20,89 @@ 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): + """ + 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): - CapBaseObject.__init__(self, date) - self.add_field('date', (basestring,datetime), date) - self.add_field('low', (int,float), low) - self.add_field('high', (int,float), high) - self.add_field('text', basestring, text) - self.add_field('unit', basestring, unit) + CapBaseObject.__init__(self, unicode(date)) + self.date = date + self.low = low + self.high = high + self.text = text + self.unit = unit 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): - CapBaseObject.__init__(self, date) - self.add_field('date', (basestring,datetime), date) - self.add_field('text', basestring, text) - self.add_field('temp', (int,float), temp) - self.add_field('unit', basestring, unit) + CapBaseObject.__init__(self, unicode(date)) + self.date = date + self.text = text + self.temp = temp + self.unit = unit class City(CapBaseObject): + """ + City where to find weather. + """ + name = StringField('Name of city') + def __init__(self, id, name): CapBaseObject.__init__(self, id) - self.add_field('name', basestring, name) + self.name = name class CityNotFound(Exception): - pass + """ + Raised when a city is not found. + """ class ICapWeather(IBaseCap): + """ + Capability for weather websites. + """ def iter_city_search(self, pattern): + """ + Look for a city. + + :param pattern: pattern to search + :type pattern: str + :rtype: iter[:class:`City`] + """ raise NotImplementedError() def get_current(self, city_id): + """ + Get current weather. + + :param city_id: ID of the city + :rtype: :class:`Current` + """ raise NotImplementedError() def iter_forecast(self, city_id): + """ + Iter forecasts of a city. + + :param city_id: ID of the city + :rtype: iter[:class:`Forecast`] + """ raise NotImplementedError() diff --git a/weboob/core/ouiboube.py b/weboob/core/ouiboube.py index 6fdf9844..036d3ed4 100644 --- a/weboob/core/ouiboube.py +++ b/weboob/core/ouiboube.py @@ -36,6 +36,19 @@ __all__ = ['Weboob'] 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' BACKENDS_FILENAME = 'backends' @@ -64,8 +77,8 @@ class Weboob(object): if os.path.isdir(old_workdir): 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)) - self.create_dir(xdg_config_home) - self.create_dir(xdg_data_home) + self._create_dir(xdg_config_home) + self._create_dir(xdg_data_home) for f in os.listdir(old_workdir): if f in Repositories.SHARE_DIRS: dest = xdg_data_home @@ -77,7 +90,7 @@ class Weboob(object): datadir = xdg_data_home self.workdir = os.path.realpath(workdir) - self.create_dir(workdir) + self._create_dir(workdir) # Repositories management self.repositories = Repositories(workdir, datadir, self.VERSION) @@ -95,7 +108,7 @@ class Weboob(object): # Storage self.storage = storage - def create_dir(self, name): + def _create_dir(self, name): if not os.path.exists(name): os.makedirs(name) elif not os.path.isdir(name): @@ -105,11 +118,15 @@ class Weboob(object): self.deinit() def deinit(self): + """ + Call this method when you stop using Weboob, to + properly unload all correctly. + """ self.unload_backends() def update(self, progress=IProgress()): """ - Update modules. + Update modules from repositories. """ self.repositories.update(progress) @@ -120,13 +137,27 @@ class Weboob(object): self.repositories.install(minfo, progress) class LoadError(Exception): + """ + Raised when a backend is unabled to load. + + :param backend_name: name of backend we can't load + :param exception: exception object + """ def __init__(self, backend_name, exception): Exception.__init__(self, unicode(exception)) self.backend_name = backend_name - def load_backend(self, module_name, 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) 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): """ - Load backends. + Load backends listed in config file. - @param caps [tuple(ICapBase)] load backends which implement all of caps - @param names [tuple(unicode)] load backends with instance name in list - @param modules [tuple(unicode)] load backends which module is in list - @param storage [IStorage] use the storage if specified - @param errors [list] if specified, store every errors in - @return [dict(str,BaseBackend)] return loaded backends + :param caps: load backends which implement all of specified caps + :type caps: tuple[:class:`weboob.capabilities.base.ICapBase`] + :param names: load backends with instance name in list + :type names: tuple[:class:`str`] + :param modules: load backends which module is in list + :type modules: tuple[:class:`str`] + :param 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 = {} if storage is None: @@ -195,6 +232,12 @@ class Weboob(object): return loaded def unload_backends(self, names=None): + """ + Unload backends. + + :param names: if specified, only unload that backends + :type names: :class:`list` + """ unloaded = {} if isinstance(names, basestring): names = [names] @@ -213,8 +256,11 @@ class Weboob(object): """ Get a backend from its name. - It raises a KeyError if not found. If you set the 'default' parameter, - the default value is returned instead. + :param name: name of backend to get + :type name: str + :param default: if specified, get this value when the backend is not found + :type default: whatever you want + :raises: :class:`KeyError` if not found. """ try: return self.backend_instances[name] @@ -225,6 +271,9 @@ class Weboob(object): raise def count_backends(self): + """ + Get number of loaded backends. + """ return len(self.backend_instances) def iter_backends(self, caps=None): @@ -233,8 +282,9 @@ class Weboob(object): Note: each backend is locked when it is returned. - @param caps Optional list of capabilities to select backends - @return iterator on selected backends. + :param caps: optional list of capabilities to select 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()): if caps is None or backend.has_caps(caps): @@ -248,17 +298,21 @@ class Weboob(object): 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; - - 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 \*\*kwargs. - @param function backend's method name, or callable object - @param backends list of backends to iterate on - @param caps iterate on backends with this caps - @param condition a condition to validate to keep the result - @return the BackendsCall object (iterable) + :param function: backend's method name, or a callable object + :type function: :class:`str` + :param backends: list of backends to iterate on + :type backends: list[:class:`str`] + :param caps: iterate on backends which implement this caps + :type caps: list[:class:`weboob.capabilities.base.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 = kwargs.pop('backends', None) @@ -296,16 +350,47 @@ class Weboob(object): return BackendsCall(backends, condition, function, *args, **kwargs) def schedule(self, interval, function, *args): + """ + Schedule an event. + + :param interval: delay before calling the function + :type interval: int + :param function: function to call + :type function: callabale + :param args: arguments to give to function + :returns: an event identificator + """ return self.scheduler.schedule(interval, function, *args) def repeat(self, interval, function, *args): + """ + Repeat a call to a function + + :param interval: interval between two calls + :type interval: int + :param function: function to call + :type function: callable + :param args: arguments to give to function + :returns: an event identificator + """ return self.scheduler.repeat(interval, function, *args) def cancel(self, ev): + """ + Cancel an event + + :param ev: the event identificator + """ return self.scheduler.cancel(ev) def want_stop(self): + """ + Plan to stop the scheduler. + """ return self.scheduler.want_stop() def loop(self): + """ + Run the scheduler loop + """ return self.scheduler.run() diff --git a/weboob/tools/application/repl.py b/weboob/tools/application/repl.py index 72de9685..9e70c92d 100644 --- a/weboob/tools/application/repl.py +++ b/weboob/tools/application/repl.py @@ -39,7 +39,7 @@ from .formatters.load import FormattersLoader, FormatterLoadError from .results import ResultsCondition, ResultsConditionError -__all__ = ['ReplApplication', 'NotEnoughArguments'] +__all__ = ['NotEnoughArguments', 'ReplApplication'] class NotEnoughArguments(Exception): @@ -354,9 +354,10 @@ class ReplApplication(Cmd, ConsoleApplication): def complete(self, text, state): """ Override of the Cmd.complete() method to: - - add a space at end of proposals - - display only proposals for words which match the - text already written by user. + + * add a space at end of proposals + * display only proposals for words which match the + text already written by user. """ super(ReplApplication, self).complete(text, state) diff --git a/weboob/tools/backend.py b/weboob/tools/backend.py index 6c00b840..425e066f 100644 --- a/weboob/tools/backend.py +++ b/weboob/tools/backend.py @@ -28,42 +28,103 @@ from weboob.tools.log import getLogger from weboob.tools.value import ValuesDict -__all__ = ['BaseBackend', 'ObjectNotAvailable'] +__all__ = ['ObjectNotAvailable', 'BackendStorage', 'BackendConfig', 'BaseBackend'] class ObjectNotAvailable(Exception): - pass + """ + Raised when an object is not available. + """ 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): self.name = name self.storage = storage 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: return self.storage.set('backends', self.name, *args) def delete(self, *args): + """ + Delete a value from the storage. + + :param args: path to delete. + """ if self.storage: return self.storage.delete('backends', self.name, *args) def get(self, *args, **kwargs): + """ + Get a value or a dict of values in storage. + + Example: + + >>> backend.storage.get('config', 'nb_of_threads') + 10 + >>> backend.storage.get('config', 'unexistant', 'path', default='lol') + 'lol' + >>> backend.storage.get('config') + {'nb_of_threads': 10, 'other_things': 'blah'} + + :param args: path to get + :param default: if specified, default value when path is not found + """ if self.storage: return self.storage.get('backends', self.name, *args, **kwargs) else: return kwargs.get('default', None) def load(self, default): + """ + Load storage. + + :param default: this is the default tree if storage is empty + :type default: :class:`dict` + """ if self.storage: return self.storage.load('backends', self.name, default) def save(self): + """ + Save storage. + """ if self.storage: return self.storage.save('backends', self.name) class BackendConfig(ValuesDict): + """ + Configuration of a backend. + + This class is firstly instanced as a :class:`weboob.tools.value.ValuesDict`, + containing some :class:`weboob.tools.value.Value` (and derivated) objects. + + Then, using the :func:`load` method will load configuration from file and + create a copy of the :class:`BackendConfig` object with the loaded values. + """ modname = None instname = None weboob = None @@ -72,12 +133,17 @@ class BackendConfig(ValuesDict): """ Load configuration from dict to create an instance. - @param weboob [Weboob] weboob object - @param modname [str] name of module - @param instname [str] name of instance of this backend - @param params [dict] parameters to load - @param nofail [bool] if true, this call can't fail. - @return [BackendConfig] + :param weboob: weboob object + :type weboob: :class:`weboob.core.ouiboube.Weboob` + :param modname: name of the module + :type modname: :class:`str` + :param instname: name of this backend + :type instname: :class:`str` + :param params: parameters to load + :type params: :class:`dict` + :param nofail: if true, this call can't fail + :type nofail: :class:`bool` + :rtype: :class:`BackendConfig` """ cfg = BackendConfig() cfg.modname = modname @@ -103,12 +169,25 @@ class BackendConfig(ValuesDict): return cfg def dump(self): + """ + Dump config in a dictionary. + + :rtype: :class:`dict` + """ settings = {} for name, value in self.iteritems(): settings[name] = value.dump() return settings def save(self, edit=True, params=None): + """ + Save backend config. + + :param edit: if true, it changes config of an existing backend + :type edit: :class:`bool` + :param params: if specified, params to merge with the ones of the current object + :type params: :class:`dict` + """ assert self.modname is not None assert self.instname is not None assert self.weboob is not None @@ -121,6 +200,22 @@ class BackendConfig(ValuesDict): 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. NAME = None # Name of the maintainer of this backend. @@ -152,7 +247,9 @@ class BaseBackend(object): OBJECTS = {} class ConfigError(Exception): - pass + """ + Raised when the config can't be loaded. + """ def __enter__(self): self.lock.acquire() @@ -184,16 +281,6 @@ class BaseBackend(object): """ 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 @property @@ -202,7 +289,7 @@ class BaseBackend(object): Attribute 'browser'. The browser is created at the first call 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: self._browser = self.create_default_browser() @@ -238,6 +325,11 @@ class BaseBackend(object): @classmethod def iter_caps(klass): + """ + Iter capabilities implemented by this backend. + + :rtype: iter[:class:`weboob.capabilities.base.IBaseCap`] + """ def iter_caps(cls): for base in cls.__bases__: if issubclass(base, IBaseCap) and base != IBaseCap: @@ -247,6 +339,9 @@ class BaseBackend(object): return iter_caps(klass) def has_caps(self, *caps): + """ + Check if this backend implements at least one of these capabilities. + """ for c in caps: if (isinstance(c, basestring) and c in [cap.__name__ for cap in self.iter_caps()]) or \ isinstance(self, c): @@ -255,7 +350,10 @@ class BaseBackend(object): 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): return (v is NotLoaded or isinstance(v, CapBaseObject) and not v.__iscomplete__()) diff --git a/weboob/tools/browser/browser.py b/weboob/tools/browser/browser.py index edba4536..f9cf2f0a 100644 --- a/weboob/tools/browser/browser.py +++ b/weboob/tools/browser/browser.py @@ -53,7 +53,8 @@ else: __all__ = ['BrowserIncorrectPassword', 'BrowserBanned', 'BrowserUnavailable', 'BrowserRetry', - 'BrowserHTTPNotFound', 'BrowserHTTPError', 'BasePage', 'BaseBrowser', 'StandardBrowser'] + 'BrowserHTTPNotFound', 'BrowserHTTPError', 'BrokenPageError', 'BasePage', + 'StandardBrowser', 'BaseBrowser'] # Exceptions @@ -135,6 +136,21 @@ def check_location(func): return inner class StandardBrowser(mechanize.Browser): + """ + Standard Browser. + + :param firefox_cookies: path to cookies sqlite file + :type firefox_cookies: str + :param parser: parser to use on HTML files + :type parser: :class:`weboob.tools.parsers.iparser.IParser` + :param history: history manager; default value is an object which + does not keep history + :type history: object + :param proxy: proxy URL to use + :type proxy: str + :param factory: mechanize factory. None to use Mechanize's default + :type factory: object + """ # ------ Class attributes -------------------------------------- @@ -161,16 +177,6 @@ class StandardBrowser(mechanize.Browser): default_features.remove('_refresh') 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) self.logger = getLogger('browser', logger) @@ -309,15 +315,16 @@ class StandardBrowser(mechanize.Browser): def buildurl(base, *args, **kwargs): """ Build an URL and escape arguments. - You can give a serie of tuples in *args (and the order is keept), or - a dict in **kwargs (but the order is lost). + + 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: + >>> buildurl('/blah.php', ('a', '&'), ('b', '=') '/blah.php?a=%26&b=%3D' >>> buildurl('/blah.php', a='&', 'b'='=') '/blah.php?b=%3D&a=%26' - """ if not args: @@ -336,11 +343,16 @@ class StandardBrowser(mechanize.Browser): """ Set a value to a form field. - @param args [dict] arguments where to look for value. - @param label [str] label in args. - @param field [str] field name. If None, use label instead. - @param value [str] value to give on field. - @param is_list [bool] the field is a list. + :param args: arguments where to look for value + :type args: dict + :param label: label in args + :type label: str + :param field: field name. If None, use label instead + :type field: str + :param value: value to give on field + :type value: str + :param is_list: the field is a list + :type is_list: bool """ try: if not field: @@ -366,6 +378,29 @@ class StandardBrowser(mechanize.Browser): class BaseBrowser(StandardBrowser): """ Base browser class to navigate on a website. + + :param username: username on website + :type username: str + :param password: password on website. If it is None, Browser will + not try to login + :type password: str + :param firefox_cookies: path to cookies sqlite file + :type firefox_cookies: str + :param parser: parser to use on HTML files + :type parser: :class:`weboob.tools.parsers.iparser.IParser` + :param history: history manager; default value is an object which + does not keep history + :type history: object + :param proxy: proxy URL to use + :type proxy: 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 -------------------------------------- @@ -406,20 +441,6 @@ class BaseBrowser(StandardBrowser): def __init__(self, username=None, password=None, firefox_cookies=None, parser=None, history=NoHistory(), proxy=None, logger=None, factory=None, get_home=True, responses_dirname=None): - """ - 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) self.page = None self.last_update = 0.0 diff --git a/weboob/tools/capabilities/bank/transactions.py b/weboob/tools/capabilities/bank/transactions.py index 17cbce56..22d72cb0 100644 --- a/weboob/tools/capabilities/bank/transactions.py +++ b/weboob/tools/capabilities/bank/transactions.py @@ -30,6 +30,9 @@ __all__ = ['FrenchTransaction'] class FrenchTransaction(Transaction): + """ + Transaction with some helpers for french bank websites. + """ PATTERNS = [] def clean_amount(self, text): @@ -61,7 +64,7 @@ class FrenchTransaction(Transaction): When calling this method, you should have defined patterns (in the 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.*)'), FrenchTransaction.TYPE_TRANSFER), (re.compile('^PRLV (?P.*)'), FrenchTransaction.TYPE_ORDER), @@ -70,8 +73,9 @@ class FrenchTransaction(Transaction): ] 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 date.isdigit() and len(date) == 8: diff --git a/weboob/tools/capabilities/messages/GenericBackend.py b/weboob/tools/capabilities/messages/GenericBackend.py index 3e8c92d8..8e8c64af 100644 --- a/weboob/tools/capabilities/messages/GenericBackend.py +++ b/weboob/tools/capabilities/messages/GenericBackend.py @@ -27,7 +27,9 @@ from weboob.tools.newsfeed import Newsfeed class GenericNewspaperBackend(BaseBackend, ICapMessages): - "GenericNewspaperBackend class" + """ + GenericNewspaperBackend class + """ MAINTAINER = 'Julien Hebert' EMAIL = 'juke@free.fr' VERSION = '0.c' diff --git a/weboob/tools/capabilities/thumbnail.py b/weboob/tools/capabilities/thumbnail.py index 757fc721..a6cb2d83 100644 --- a/weboob/tools/capabilities/thumbnail.py +++ b/weboob/tools/capabilities/thumbnail.py @@ -17,13 +17,23 @@ # You should have received a copy of the GNU Affero General Public License # along with weboob. If not, see . -from weboob.capabilities.base import CapBaseObject, NotLoaded +from weboob.capabilities.base import CapBaseObject, NotLoaded, StringField, BytesField + + +__all__ = ['Thumbnail'] + class Thumbnail(CapBaseObject): + """ + Thumbnail of an image. + """ + + url = StringField('URL to photo thumbnail') + data = BytesField('Data') + def __init__(self, url): CapBaseObject.__init__(self, url) - self.add_field('url', basestring, url.replace(' ', '%20')) - self.add_field('data', str) + self.url = url.replace(' ', '%20') def __str__(self): return self.url diff --git a/weboob/tools/parsers/lxmlparser.py b/weboob/tools/parsers/lxmlparser.py index 9d166345..7691b969 100644 --- a/weboob/tools/parsers/lxmlparser.py +++ b/weboob/tools/parsers/lxmlparser.py @@ -60,14 +60,18 @@ class LxmlHtmlParser(IParser): """ 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 selector [str] CSS or XPath expression - @param method [str] (cssselect|xpath) - @param nb [int] number of elements expected to be found. - Use None for undefined number, and 'many' for 1 to infinite. - @return one or many Element + :param element: element on which to apply selector + :type element: object + :param selector: CSS or XPath expression + :type selector: str + :param method: (cssselect|xpath) + :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': results = element.cssselect(selector)