# HG changeset patch # User Sandro Knauß # Date 1333031260 -7200 # Node ID 0a5eb5aac0bee599e372e41e31e5d76e1b3d76c8 # Parent 31114e40178d6fd06cb9c77206cee9eb5df6951b iro.model: adding docstring diff -r 31114e40178d -r 0a5eb5aac0be iro/model/dbdefer.py --- 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 diff -r 31114e40178d -r 0a5eb5aac0be iro/model/decorators.py --- 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 diff -r 31114e40178d -r 0a5eb5aac0be iro/model/job.py --- 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.""" diff -r 31114e40178d -r 0a5eb5aac0be iro/model/message.py --- 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 ""%(self.subject,self.body,self.frm) + """string representation of the class. + + :returns: ```` + """ + return ""%(self.subject,self.body,self.frm) + +__all__=["Message", "SMS", "Fax", "Mail"] diff -r 31114e40178d -r 0a5eb5aac0be iro/model/offer.py --- 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) diff -r 31114e40178d -r 0a5eb5aac0be iro/model/pool.py --- 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: diff -r 31114e40178d -r 0a5eb5aac0be iro/model/schema.py --- 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 (default!=None) 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. provider, typ und route 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 isBilled=0.""" + """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 (init, started, sending, sended oder error). info 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: ```` + """ try: - return ""%(self.id,self.info, self.status, self.user_id) + return ""%(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. ng_kunde 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: ```` + """ try: return ""%(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()