--- a/iro/model/dbdefer.py Thu Mar 29 13:46:37 2012 +0200
+++ b/iro/model/dbdefer.py Thu Mar 29 16:27:40 2012 +0200
@@ -7,8 +7,16 @@
import inspect
class DBDefer(object):
- '''a twisted sqlalchemy connector this Decorator adds a session parameter, with a valid session connection'''
+ '''a twisted sqlalchemy connector.
+
+ This is used as a Decorator class.
+ It adds a session parameter to the function with a valid session.
+ If the session parmaeter is used in calling this function only calls a commit after running the function, instead of createing a new session.'''
def __init__(self, engine, autocommit=False):
+ """
+ :param `sqlalchemy.engine.base.Engine` engine: a valid sqlalchemy engine object (normally created via :func:`sqlalchemy.create_engine`).
+ :param boolean autocommit: autocommit after running the function.
+ """
self.autocommit=autocommit
self.engine = engine
@@ -53,8 +61,10 @@
return wrap
dbdefer=DBDefer(None)
+"""the decorator to use. Use :func:`setEngine` to set a valid engine after program has started."""
def setEngine(engine,autocommit=False):
+ """set the engine and autocommit for the decorator (see :class:`DBDefer`)."""
dbdefer.engine = engine
dbdefer.autocommit = autocommit
--- a/iro/model/decorators.py Thu Mar 29 13:46:37 2012 +0200
+++ b/iro/model/decorators.py Thu Mar 29 16:27:40 2012 +0200
@@ -1,5 +1,11 @@
+"""All decorators, that are created by this package.
+
+Imports:
+
+- :func:`.user.vUser` -- a validator for apikeys.
+- :func:`.pool.runInDBPool` -- runs a actual function in dbpool.
+"""
import types
-from decorator import decorator
from .user import vUser
from .schema import Offer
@@ -10,6 +16,17 @@
@dbdefer
def vRoute(session, value, field, typ, allowString=True, allowList=True):
+ """ a validator to test a valid route. use with :func:`iro.validate.validate`.
+
+ :param session: a valid session object (is created by decorator :func:`iro.model.dbdefer.dbdefer`)
+ :param value: the value to test
+ :param string field: the field that is tested (only used to get a propper error message).
+ :param string typ: a typ to test the route in
+ :param boolean allowString: a single route is allowd.
+ :param boolean allowList: a list of routes is allowed.
+ :return: *value*, if value is a valid route for a given typ.
+ :raises: :exc:`iro.error.ValidateException`
+ """
str_ = False
ret = []
@@ -34,6 +51,15 @@
@dbdefer
def vTyp(value,field, session):
+ """ a validator to test a valid typ. use with :func:`iro.validate.validate`.
+
+ :param session: a valid session object (is created by decorator :func:`iro.model.dbdefer.dbdefer`)
+ :param value: the value to test
+ :param string field: the field that is tested (only used to get a propper error message).
+ :return: *value*, if value is a valid typ.
+ :raises: :exc:`iro.error.ValidateException`
+ """
+
for typ in Offer.typs(session):
if value == typ[0]:
break
--- a/iro/model/job.py Thu Mar 29 13:46:37 2012 +0200
+++ b/iro/model/job.py Thu Mar 29 16:27:40 2012 +0200
@@ -8,12 +8,25 @@
from .dbdefer import dbdefer
class ExJob:
- '''One Job is a class that handles one job. One Job has multiple tasks.'''
+ ''' A ExJob object represents a message to multiple recipients over multiple offers to send.
+
+ One single message to one recipient is handeld in :class:`iro.controller.task.Task`.
+ This class holds connections to all tasks.
+ This class is responsiple to update the status in database of one job and updating the bill.
+ '''
synchronized = ["incStatus", "_status"]
def __init__(self, dbjob, recipients, message, offers):
- self.dbjob = dbjob #Connection to mysql job element (id)
+ """Constructor of ExJob.
+
+ :param dbjob: primary key of the job element in database
+ :param list recipients: list of all recipients
+ :param `iro.model.message.Message` message: message to send
+ :param list offers: list of all possible offers to send message over
+ """
+
+ self.dbjob = dbjob #Connection to database job element (id)
self.message = message
self.recipients = recipients
self.offers = offers
@@ -23,13 +36,23 @@
log.msg("Job(%s) created."%(self.dbjob))
def addTask(self,task):
+ """adding a task to tasks dict - key is the recipient.
+
+ :param `iro.controller.task.Task` task: a task
+ """
self.tasks[task.recipient] = task
def incStatus(self):
+ """increments the processed messages (function is threadsafe)."""
self.c += 1
return self.c
def _status(self, session, status):
+ """updates the status of the database object (function is threadsafe).
+
+ :param session: a valid database session
+ :param string status: new status
+ """
job = schema.Job.get(session, self.dbjob)
if self.status == "error":
return
@@ -42,6 +65,10 @@
@dbdefer
def setStatus(self, session, task, status):
+ """callback of one task.
+
+ This function updates the database object and the bill.
+ """
c = self.incStatus()
job = schema.Job.get(session, self.dbjob)
@@ -59,6 +86,10 @@
@dbdefer
def setError(self, session, task, err):
+ """errback for one task.
+
+ This function updates the database object.
+ """
self.incStatus()
if self.status != "error":
self._status(session,"error")
@@ -67,9 +98,19 @@
threadable.synchronize(ExJob)
class ExJobs(dict, MutableMapping):
-
+ """ a dict to handle all jobs.
+ """
@dbdefer
def create(self, session, user, recipients, message, offers, info=None):
+ """creates on new Job.
+
+ :param session: a valid session ( created by decorator :func:`iro.model.dbdefer.dbdefer`)
+ :param `iro.model.schema.User` user: a user object
+ :param list recipients: list of all recipients
+ :param `iro.model.message.Message` message: message to send
+ :param list offers: a list of offers ( list will be reduced to the allowed offers for the **user** -- using :func:`iro.model.offer.extendProvider`)
+ :returns: the new job
+ """
user = session.merge(user)
job = schema.Job(info=info, status="started")
user.jobs.append(job)
@@ -80,3 +121,4 @@
return self[job.id]
exJobs = ExJobs()
+"""the dict of all available jobs."""
--- a/iro/model/message.py Thu Mar 29 13:46:37 2012 +0200
+++ b/iro/model/message.py Thu Mar 29 16:27:40 2012 +0200
@@ -1,36 +1,65 @@
# -*- coding: utf-8 -*-
-
+"""All available message typs to send send.
+"""
from email.mime.text import MIMEText
from email.header import Header
from email.Utils import formatdate
class Message:
+ """ Baseclass for all different message typs."""
def __init__(self,content, typ="Message"):
+ """Constructor of Message class.
+
+ :param content: content of the message
+ :param string typ: typ of the message
+
+ .. automethod:: __eq__
+ .. automethod:: __neq__
+ """
self.content=content
self.typ = typ
def getContent(self):
+ """returns the content of the message"""
return self.content
def __eq__(self,other):
+ """return ``True`` if **other** has the same content."""
return self.content == other.content
- def __ne__(self,other):
+ def __neq__(self,other):
+ """return ``False`` if **other** has the same content."""
return not self.__eq__(other)
class SMS(Message):
+ """ A representation of one SMS"""
def __init__(self, cont, from_ = None):
+ """Constructor of SMS class.
+
+ :param string cont: SMS content
+ :param string from_: the telnumber from the SMS should be sended.
+ """
Message.__init__(self, cont.encode("utf-8"), typ="sms")
self.from_ = from_
class Fax(Message):
+ """A representation of one fax."""
def __init__(self,header,cont,attachments=[]):
+ """Constructor of one fax.
+
+ :param string header: Headline of fax
+ :param string cont: fax content
+ :param list attachments: attachments of fax
+ """
Message.__init__(self,cont.encode("utf-8"),typ="fax")
self.header=header
self.attachments=attachments
def getAttachment(self,i):
+ """returns a attachment
+ :param integer i: the i-th attachment
+ """
return self.attachments[i]
def __eq__(self,other):
@@ -51,7 +80,16 @@
class Mail(Message):
+ """A representation of one Mail"""
def __init__(self, subject, body, frm):
+ """Constructor of one mail.
+
+ :param string subject: subject of the mail
+ :param string body: body of the mail
+ :param string frm: mailaddress to send mail from
+
+ .. automethod:: __repr__
+ """
con = MIMEText(body.encode("utf-8"), _charset='utf-8')
sub = Header(subject.encode('utf-8'), 'utf-8')
con['Subject'] = sub
@@ -62,9 +100,11 @@
Message.__init__(self, con, typ='mail')
def as_string(self):
+ """returns created mail"""
return self.content.as_string()
def getFrom(self):
+ """returns the from mailaddress"""
return self.frm
def __eq__(self,other):
@@ -77,4 +117,10 @@
return True
def __repr__(self):
- return "<Mail(%s,%s,%s)>"%(self.subject,self.body,self.frm)
+ """string representation of the class.
+
+ :returns: ``<Mail(subject, body, frm)>``
+ """
+ return "<Mail(%s, %s, %s)>"%(self.subject,self.body,self.frm)
+
+__all__=["Message", "SMS", "Fax", "Mail"]
--- a/iro/model/offer.py Thu Mar 29 13:46:37 2012 +0200
+++ b/iro/model/offer.py Thu Mar 29 16:27:40 2012 +0200
@@ -6,6 +6,17 @@
@dbdefer
def extendProvider(session, user, typ, ps):
+ """extend and reduce the offer list to allowed routes for **user**.
+
+ - extend the "default" to the default offerlist of the **user**.
+ - extend a Provider name to all routes of that provider.
+
+ :param session: a valid session ( created by decorator :func:`iro.model.dbdefer.dbdefer`)
+ :param `iro.model.schema.User` user: a user object
+ :param string typ: typ of the message
+ :param ps: a list of strings or a string, each one offer name or provider name
+ :return: a extended an reduced offer list
+ """
user = session.merge(user)
ret = []
if ps == "default" or ps == ["default"]:
@@ -22,6 +33,10 @@
@dbdefer
def loadOffers(session):
+ """loading Offers from database and configuration file and store them in :attr:`~iro.model.offer.offers` and :attr:`~iro.model.offer.providers`.
+
+ :param session: a valid session ( created by decorator :func:`iro.model.dbdefer.dbdefer`)
+ """
offers.clear()
providers.clear()
for provider in ( s for s in configParser.sections() if not s in ["main",]):
@@ -33,7 +48,11 @@
providers[provider]=p
offers={}
+"""A dict of all available offers -- key is the offer name"""
+
providers={}
+"""A dict of all available providers -- key is the provider name"""
+
configParser.registerReload(loadOffers)
--- a/iro/model/pool.py Thu Mar 29 13:46:37 2012 +0200
+++ b/iro/model/pool.py Thu Mar 29 16:27:40 2012 +0200
@@ -1,18 +1,23 @@
from decorator import decorator
import sqlalchemy
class Data:
+ """A very simple class to save the thread pool for database requests"""
def __init__(self):
self.pool = None
data = Data()
-#a valid dbDefer decorator
+"""holds connection to the actual thread pool for the database requests"""
def setPool(pool):
+ """setting the thread pool"""
data.pool = pool
@decorator
def runInDBPool(f,*args,**kwargs):
- """Decorator to run DB queries in Twisted's thread pool"""
+ """Decorator to run DB queries in Twisted's thread pool.
+
+ If last argument is a session object, the function is called directly.
+ """
if isinstance(args[-1],sqlalchemy.orm.session.Session):
return f(*args,**kwargs)
else:
--- a/iro/model/schema.py Thu Mar 29 13:46:37 2012 +0200
+++ b/iro/model/schema.py Thu Mar 29 16:27:40 2012 +0200
@@ -17,21 +17,32 @@
Base = declarative_base()
class Userright(Base):
- """Über welche Routen darf ein Benutzer Daten verschicken und welches sind die Standardrouten (<em>default!=None</em>) für den Benuter. Geordnert werden die Standardrouten nach default."""
+ """Allowed offers for one user. Default routes are sorted by **default** value."""
__tablename__ = 'userright'
user_name = Column('user', String(100), ForeignKey('apiuser.name'), primary_key=True)
+ """username"""
offer_name = Column('offer', String(100), ForeignKey('offer.name'), primary_key=True)
+ """offername"""
default = Column(Integer)
+ """sorting defaults routes with this value"""
offer = relationship("Offer")
+ """connected :class:`Offer` object"""
user = relationship("User")
+ """connected :class:`User` object"""
def __init__(self, offer, default=None):
+ """Constructor of Userright class.
+
+ :param `Offer` offer: a offer object
+ :param integer default: default value
+ """
+
self.offer = offer
self.default = default
@property
def bill(self):
- '''getting a list of unbilled messages grouped by Job.info'''
+ """returns a list of unbilled messages grouped by Job.info"""
query = [ func.count(Message.id).label('anz'), # anz of messages
func.sum(Message.price).label("price"), # price of the messages
Job.info.label('info'), # info tag
@@ -45,88 +56,177 @@
return object_session(self).query(*query).filter(and_(*filters)).group_by(Job.info)
class Offer(Base):
- """Alle Routen über die SMS, Faxe und Mails verschickt werden könnnen. <em>provider</em>, <em>typ</em> und <em>route</em> werden verwendet, um die entsprechenden Zugangsdaten laden zu können."""
+ """All possible Offers over a Message can be sended. **provider**, **typ** and **route** are used to get the data form configuration file."""
__tablename__ = "offer"
name = Column(String(100), primary_key=True)
+ """name of the offer"""
provider = Column(String(100))
+ """provider name"""
route = Column(String(100))
+ """route of the provider"""
typ = Column(String(100))
+ """typ of message"""
+
+ def __init__(self, **kwargs):
+ Base.__init__(self,**kwargs)
@classmethod
def get(cls, session, provider, route, typ):
+ """returns a Offer object."""
return session.query(cls).filter(and_(cls.provider==provider, cls.route==route, cls.typ==typ)).first()
@classmethod
def routes(cls, session, typ):
+ """returns a query object of all possible offers.
+
+ :param string typ: get all offers that support this typ
+ """
return session.query(cls).filter_by(typ=typ)
@classmethod
def typs(cls, session):
+ """returns a list of all possible types.
+ """
return session.query(cls.typ).distinct()
@classmethod
def providers(cls, session, typ):
+ """returns a list of all possible providers.
+
+ :param string typ: get all providers that support this typ
+ """
return session.query(cls.provider).filter_by(typ=typ).distinct()
class Message(Base):
- """Wenn ein Vorgang von Iro Kosten erzeugt hat wird eine neue Zeile eingefügt. Solange nicht bezahlt wurde ist <em>isBilled=0</em>."""
+ """A message that has created costs.
+
+ **isBilled** is False since the bill is paid.
+ """
__tablename__ = "message"
id = Column(Integer, Sequence('message_id_seq'), primary_key=True)
+ """primary key of the message"""
+
recipient = Column(String(100))
+ """string representation of the recipient"""
+
isBilled = Column(Boolean)
+ """is bill paid?"""
+
date = Column(DateTime)
+ """date of sending the message"""
+
price = Column(Numeric(8,4))
+ """price of sending the message"""
+
count = Column(Integer)
+ """Count of sended messages"""
exID = Column(String(100))
+ """external API id """
+
job_id = Column("job", String(40), ForeignKey('job.id'))
+ """id of the connected job"""
job = relationship("Job", backref=backref('messages'))
+ """connected :class:`Job` object"""
+
offer_id = Column("offer",String(100), ForeignKey('offer.name'))
+ """sended message over this offer woth ithe offer.name"""
+
offer = relationship("Offer", backref=backref('messages'))
+ """connected :class:`Offer` object"""
+
+ def __init__(self, **kwargs):
+ Base.__init__(self,**kwargs)
class Job(Base):
- """Ein kompletter Auftrag, der an Iro zum verschicken übergeben wird. Status zeigt den generellen Status des Auftrages an (<em>init</em>, <em>started</em>, <em>sending</em>, <em>sended</em> oder <em>error</em>). <em>info</em> wird verwendet um dem Benutzer eine Möglickeit zu geben verschiede Auftragsgruppen zu erstellen."""
+ """A complete Job.
+
+ - **status** show the status of the job (``init``, ``started``, ``sending``, ``sended`` or ``error``).
+ - **info** is used to make it possible to create diffrent billing groups for user.
+ """
__tablename__ = "job"
id = Column(Integer, Sequence('job_id_seq'), primary_key=True)
+ """job id"""
info = Column(String(100))
+ """job info, for billing porpuse"""
status = Column(Enum("init","started","sending","sended","error"))
+ """status of a job"""
user_id = Column("user", String(100), ForeignKey('apiuser.name'))
+ """connected user id"""
+
user = relationship("User", backref=backref('jobs'))
+ """connected :class:`User` object"""
+
+ def __init__(self, **kwargs):
+ """
+ .. automethod:: __repr__
+ """
+ Base.__init__(self,**kwargs)
@property
def extend(self):
+ """returns the connected :class:`iro.model.job.ExJob`"""
return job.exJobs[self.id]
def __repr__(self):
+ """string representation of the Job class.
+
+ :return: ``<Job('id' ,'info', 'status', 'user_id')>``
+ """
try:
- return "<Job('%s','%s','%s','%s')>"%(self.id,self.info, self.status, self.user_id)
+ return "<Job('%s' ,'%s', '%s', '%s')>"%(self.id,self.info, self.status, self.user_id)
except DetachedInstanceError:
return Base.__repr__(self)
@classmethod
def get(cls, session, id):
+ """returns a job object from a given id"""
return session.query(cls).filter_by(id=id).first()
class User(Base):
- """Die Benutzerdatenbank von Iro. <em>ng_kunde</em> ist der verknüpfte netzguerilla.net Benutzer, der die Rechnung zahlt."""
+ """One user in iro"""
__tablename__ = "apiuser"
+
name = Column(String(100), primary_key=True)
+ """Username"""
+
ng_kunde = Column(Integer)
+ """Connection to the netzguerilla userdatabase"""
+
apikey = Column(String(50),unique=True)
+ """apikey only [0-9a-f]"""
+
rights = relationship('Userright')
+ """all allowed offers to send with."""
+
def __init__(self, name, apikey):
+ """Constructor of User class.
+
+ :param string name: username
+ :param string apikey: apikey for the user
+
+ .. automethod:: __repr__
+ """
self.name=name
self.apikey=apikey
def __repr__(self):
+ """string representation of the user class.
+
+ :return: ``<User('name', 'apikey')>``
+ """
try:
return "<User('%s','%s')>"%(self.name,self.apikey)
except DetachedInstanceError:
return Base.__repr__(self)
def routes(self, typ, default = False):
- '''returns a query object to get all possible routes for a given typ'''
+ """returns a query object to get all possible routes for a given typ
+
+ :param string typ: the typ
+ :param boolean default: use only default routes
+ """
filters=[User.name == self.name,
Offer.typ == typ,
]
@@ -135,7 +235,11 @@
return object_session(self).query(Userright.offer_name).join(Offer,User).filter(and_(*filters)).order_by(Userright.default)
def providers(self, typ, default = False):
- '''return a query object for all possible providers for a given typ'''
+ """return a query object for all possible providers for a given typ
+
+ :param string typ: the typ
+ :param boolean default: use only default routes
+ """
filters=[User.name == self.name,
Offer.typ == typ,
]
@@ -144,8 +248,14 @@
return object_session(self).query(Offer.provider).join(Userright,User).filter(and_(*filters))
def has_right(self, typ, offer_name = None, provider = None, route = None):
- '''returns offer_name, if the user is allowed to use offer otherwise None
- ->raise sqlalchemy.orm.exc.MultipleResultsFound if not a single offer match'''
+ """if a user has the right to use a offer, provider e. al. (arguments are and connected).
+
+ :param string typ: the typ
+ :param string offer_name: offer name
+ :param string provider: provider name
+ :param string route: a route name
+ :return: offer_name or None (not allwoed)
+ :raises: :class:`sqlalchemy.orm.exc.MultipleResultsFound` if not a single offer match"""
filters=[User.name == self.name,
Offer.typ == typ,
]
@@ -159,6 +269,12 @@
return object_session(self).query(Userright.offer_name).join(Offer,User).filter(and_(*filters)).scalar()
def job(self, id):
+ """returns a job object.
+
+ :param integer id: id of a Job
+ :return: :class:`Job`
+ :raises: :exc:`iro.error.JobNotFound`
+ """
job = object_session(self).query(Job).join(User).filter(and_(User.name == self.name, Job.id==id)).first()
if job is None:
raise JobNotFound()