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

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

View file

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

View file

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

View file

@ -10,8 +10,7 @@ def genapi():
os.chdir('api')
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)

View file

@ -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

View file

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

View file

@ -3,21 +3,13 @@ Overview
Weboob (Web Out Of Browsers) provides:
* :doc:`applications` to interact with websites
* :doc:`backends`, each one handles a specific website
* :doc:`applications <api/tools/application/index>` to interact with websites
* :doc:`backends <api/tools/backend>`, each one handles a specific website
* a :doc:`core library <api/core/index>` providing all the features needed by backends
* :doc:`tools <api/tools/index>` 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
------------

View file

@ -18,23 +18,32 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject
from .base import IBaseCap, CapBaseObject, StringField, Field
__all__ = ['ICapAccount']
__all__ = ['AccountRegisterError', 'Account', 'StatusField', 'ICapAccount']
class AccountRegisterError(Exception):
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()

View file

@ -18,30 +18,45 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, date
from datetime import date, datetime
from .base import CapBaseObject
from .base import CapBaseObject, Field, StringField, DateField, FloatField, IntField
from .collection import ICapCollection
__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"<Account id=%r label=%r>" % (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 "<Transaction date='%s' label='%s' amount=%s>" % (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()

View file

@ -18,14 +18,28 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from weboob.tools.misc import iter_fields
import datetime
from dateutil.parser import parse as parse_dt
from copy import deepcopy
from weboob.tools.misc import to_unicode
from weboob.tools.ordereddict import OrderedDict
__all__ = ['FieldNotFound', 'IBaseCap', 'NotAvailable', 'NotLoaded',
'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)
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

View file

@ -17,74 +17,133 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
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()

View file

@ -17,30 +17,49 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime, timedelta
from .base import IBaseCap, CapBaseObject
from .base import IBaseCap, CapBaseObject, Field, StringField, DateField, \
IntField, DeltaField
__all__ = ['ICapBugTracker']
__all__ = ['IssueError', 'Project', 'User', 'Version', 'Status', 'Attachment',
'Change', 'Update', 'Issue', 'Query', 'ICapBugTracker']
class IssueError(Exception):
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 '<Project %r>' % 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 '<User %r>' % 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 '<Version %r>' % 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 '<Status %r>' % 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 '<Attachment %r>' % 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 '<Update %r>' % 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()

View file

@ -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()

View file

@ -18,14 +18,17 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject
from .base import IBaseCap, CapBaseObject, Field, StringField, BytesField, IntField
from weboob.tools.ordereddict import OrderedDict
__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

View file

@ -18,37 +18,74 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
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()

View file

@ -18,55 +18,103 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
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()

View file

@ -17,31 +17,39 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
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()

View file

@ -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()

View file

@ -18,27 +18,40 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject
from .base import IBaseCap, CapBaseObject, StringField, FloatField
__all__ = ['IpLocation', 'ICapGeolocIp']
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()

View file

@ -18,19 +18,23 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
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'<HousingPhoto "%s" data=%do>' % (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()

View file

@ -17,40 +17,59 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
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):

View file

@ -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 '<Message id=%r title=%r date=%r from=%r>' % (
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()

View file

@ -18,27 +18,34 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject, NotLoaded
from .base import IBaseCap, CapBaseObject, NotLoaded, Field, StringField
__all__ = ['PasteNotFound', 'BasePaste', 'ICapPaste']
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()

View file

@ -18,17 +18,18 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from .base import IBaseCap, CapBaseObject
from .base import IBaseCap, CapBaseObject, Field, StringField
__all__ = ['Emission', 'Stream', 'Radio', 'ICapRadio']
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()

View file

@ -17,41 +17,72 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import datetime
from .base import IBaseCap, CapBaseObject
from .base import IBaseCap, CapBaseObject, Field, StringField, FloatField, DateField, IntField
__all__ = ['ICapTorrent', 'Torrent']
__all__ = ['MagnetOnly', 'Torrent', 'ICapTorrent']
class MagnetOnly(Exception):
"""
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()

View file

@ -18,76 +18,98 @@
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from datetime import time, datetime, timedelta
import datetime
from .base import IBaseCap, CapBaseObject
from .base import IBaseCap, CapBaseObject, StringField, TimeField, DeltaField, DateField
__all__ = ['Departure', 'ICapTravel', 'Station']
__all__ = ['Station', 'Departure', 'RoadStep', 'RoadmapError', 'RoadmapFilters', 'ICapTravel']
class Station(CapBaseObject):
"""
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 "<Station id=%r name=%r>" % (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"<Departure id=%r type=%r time=%r departure=%r arrival=%r>" % (
self.id, self.type, self.time.strftime('%H:%M'), self.departure_station, self.arrival_station)
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()

View file

@ -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 <http://www.gnu.org/licenses/>.
from datetime import datetime, timedelta
from datetime import timedelta
from .base import IBaseCap, CapBaseObject, NotAvailable
from .base import IBaseCap, CapBaseObject, NotAvailable, StringField, Field, DateField
from weboob.tools.capabilities.thumbnail import Thumbnail
__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()

View file

@ -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()

View file

@ -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()

View file

@ -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,8 +354,9 @@ 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
* 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)

View file

@ -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__())

View file

@ -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

View file

@ -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<text>.*)'), FrenchTransaction.TYPE_TRANSFER),
(re.compile('^PRLV (?P<text>.*)'), 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:

View file

@ -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'

View file

@ -17,13 +17,23 @@
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from weboob.capabilities.base import CapBaseObject, NotLoaded
from weboob.capabilities.base import CapBaseObject, NotLoaded, StringField, BytesField
__all__ = ['Thumbnail']
class Thumbnail(CapBaseObject):
"""
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

View file

@ -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)