]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
Add missing "utils" import (used in security conditional)
[dak.git] / daklib / dbconn.py
1 #!/usr/bin/python
2
3 """ DB access class
4
5 @contact: Debian FTPMaster <ftpmaster@debian.org>
6 @copyright: 2000, 2001, 2002, 2003, 2004, 2006  James Troup <james@nocrew.org>
7 @copyright: 2008-2009  Mark Hymers <mhy@debian.org>
8 @copyright: 2009  Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009  Mike O'Connor <stew@debian.org>
10 @license: GNU General Public License version 2 or later
11 """
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ################################################################################
28
29 # < mhy> I need a funny comment
30 # < sgran> two peanuts were walking down a dark street
31 # < sgran> one was a-salted
32 #  * mhy looks up the definition of "funny"
33
34 ################################################################################
35
36 import os
37 import psycopg2
38 import traceback
39
40 from inspect import getargspec
41
42 from sqlalchemy import create_engine, Table, MetaData, select
43 from sqlalchemy.orm import sessionmaker, mapper, relation
44
45 # Don't remove this, we re-export the exceptions to scripts which import us
46 from sqlalchemy.exc import *
47 from sqlalchemy.orm.exc import NoResultFound
48
49 # Only import Config until Queue stuff is changed to store its config
50 # in the database
51 import utils
52 from config import Config
53 from singleton import Singleton
54 from textutils import fix_maintainer
55
56 ################################################################################
57
58 __all__ = ['IntegrityError', 'SQLAlchemyError']
59
60 ################################################################################
61
62 def session_wrapper(fn):
63     def wrapped(*args, **kwargs):
64         private_transaction = False
65         session = kwargs.get('session')
66
67         # No session specified as last argument or in kwargs, create one.
68         if session is None and len(args) == len(getargspec(fn)[0]) - 1:
69             private_transaction = True
70             kwargs['session'] = DBConn().session()
71
72         try:
73             return fn(*args, **kwargs)
74         finally:
75             if private_transaction:
76                 # We created a session; close it.
77                 kwargs['session'].close()
78
79     return wrapped
80
81 ################################################################################
82
83 class Architecture(object):
84     def __init__(self, *args, **kwargs):
85         pass
86
87     def __eq__(self, val):
88         if isinstance(val, str):
89             return (self.arch_string== val)
90         # This signals to use the normal comparison operator
91         return NotImplemented
92
93     def __ne__(self, val):
94         if isinstance(val, str):
95             return (self.arch_string != val)
96         # This signals to use the normal comparison operator
97         return NotImplemented
98
99     def __repr__(self):
100         return '<Architecture %s>' % self.arch_string
101
102 __all__.append('Architecture')
103
104 @session_wrapper
105 def get_architecture(architecture, session=None):
106     """
107     Returns database id for given C{architecture}.
108
109     @type architecture: string
110     @param architecture: The name of the architecture
111
112     @type session: Session
113     @param session: Optional SQLA session object (a temporary one will be
114     generated if not supplied)
115
116     @rtype: Architecture
117     @return: Architecture object for the given arch (None if not present)
118     """
119
120     q = session.query(Architecture).filter_by(arch_string=architecture)
121
122     try:
123         return q.one()
124     except NoResultFound:
125         return None
126
127 __all__.append('get_architecture')
128
129 @session_wrapper
130 def get_architecture_suites(architecture, session=None):
131     """
132     Returns list of Suite objects for given C{architecture} name
133
134     @type source: str
135     @param source: Architecture name to search for
136
137     @type session: Session
138     @param session: Optional SQL session object (a temporary one will be
139     generated if not supplied)
140
141     @rtype: list
142     @return: list of Suite objects for the given name (may be empty)
143     """
144
145     q = session.query(Suite)
146     q = q.join(SuiteArchitecture)
147     q = q.join(Architecture).filter_by(arch_string=architecture).order_by('suite_name')
148
149     ret = q.all()
150
151     return ret
152
153 __all__.append('get_architecture_suites')
154
155 ################################################################################
156
157 class Archive(object):
158     def __init__(self, *args, **kwargs):
159         pass
160
161     def __repr__(self):
162         return '<Archive %s>' % self.archive_name
163
164 __all__.append('Archive')
165
166 @session_wrapper
167 def get_archive(archive, session=None):
168     """
169     returns database id for given c{archive}.
170
171     @type archive: string
172     @param archive: the name of the arhive
173
174     @type session: Session
175     @param session: Optional SQLA session object (a temporary one will be
176     generated if not supplied)
177
178     @rtype: Archive
179     @return: Archive object for the given name (None if not present)
180
181     """
182     archive = archive.lower()
183
184     q = session.query(Archive).filter_by(archive_name=archive)
185
186     try:
187         return q.one()
188     except NoResultFound:
189         return None
190
191 __all__.append('get_archive')
192
193 ################################################################################
194
195 class BinAssociation(object):
196     def __init__(self, *args, **kwargs):
197         pass
198
199     def __repr__(self):
200         return '<BinAssociation %s (%s, %s)>' % (self.ba_id, self.binary, self.suite)
201
202 __all__.append('BinAssociation')
203
204 ################################################################################
205
206 class DBBinary(object):
207     def __init__(self, *args, **kwargs):
208         pass
209
210     def __repr__(self):
211         return '<DBBinary %s (%s, %s)>' % (self.package, self.version, self.architecture)
212
213 __all__.append('DBBinary')
214
215 @session_wrapper
216 def get_suites_binary_in(package, session=None):
217     """
218     Returns list of Suite objects which given C{package} name is in
219
220     @type source: str
221     @param source: DBBinary package name to search for
222
223     @rtype: list
224     @return: list of Suite objects for the given package
225     """
226
227     return session.query(Suite).join(BinAssociation).join(DBBinary).filter_by(package=package).all()
228
229 __all__.append('get_suites_binary_in')
230
231 @session_wrapper
232 def get_binary_from_id(id, session=None):
233     """
234     Returns DBBinary object for given C{id}
235
236     @type id: int
237     @param id: Id of the required binary
238
239     @type session: Session
240     @param session: Optional SQLA session object (a temporary one will be
241     generated if not supplied)
242
243     @rtype: DBBinary
244     @return: DBBinary object for the given binary (None if not present)
245     """
246
247     q = session.query(DBBinary).filter_by(binary_id=id)
248
249     try:
250         return q.one()
251     except NoResultFound:
252         return None
253
254 __all__.append('get_binary_from_id')
255
256 @session_wrapper
257 def get_binaries_from_name(package, version=None, architecture=None, session=None):
258     """
259     Returns list of DBBinary objects for given C{package} name
260
261     @type package: str
262     @param package: DBBinary package name to search for
263
264     @type version: str or None
265     @param version: Version to search for (or None)
266
267     @type package: str, list or None
268     @param package: Architectures to limit to (or None if no limit)
269
270     @type session: Session
271     @param session: Optional SQL session object (a temporary one will be
272     generated if not supplied)
273
274     @rtype: list
275     @return: list of DBBinary objects for the given name (may be empty)
276     """
277
278     q = session.query(DBBinary).filter_by(package=package)
279
280     if version is not None:
281         q = q.filter_by(version=version)
282
283     if architecture is not None:
284         if not isinstance(architecture, list):
285             architecture = [architecture]
286         q = q.join(Architecture).filter(Architecture.arch_string.in_(architecture))
287
288     ret = q.all()
289
290     return ret
291
292 __all__.append('get_binaries_from_name')
293
294 @session_wrapper
295 def get_binaries_from_source_id(source_id, session=None):
296     """
297     Returns list of DBBinary objects for given C{source_id}
298
299     @type source_id: int
300     @param source_id: source_id to search for
301
302     @type session: Session
303     @param session: Optional SQL session object (a temporary one will be
304     generated if not supplied)
305
306     @rtype: list
307     @return: list of DBBinary objects for the given name (may be empty)
308     """
309
310     return session.query(DBBinary).filter_by(source_id=source_id).all()
311
312 __all__.append('get_binaries_from_source_id')
313
314 @session_wrapper
315 def get_binary_from_name_suite(package, suitename, session=None):
316     ### For dak examine-package
317     ### XXX: Doesn't use object API yet
318
319     sql = """SELECT DISTINCT(b.package), b.version, c.name, su.suite_name
320              FROM binaries b, files fi, location l, component c, bin_associations ba, suite su
321              WHERE b.package=:package
322                AND b.file = fi.id
323                AND fi.location = l.id
324                AND l.component = c.id
325                AND ba.bin=b.id
326                AND ba.suite = su.id
327                AND su.suite_name=:suitename
328           ORDER BY b.version DESC"""
329
330     return session.execute(sql, {'package': package, 'suitename': suitename})
331
332 __all__.append('get_binary_from_name_suite')
333
334 @session_wrapper
335 def get_binary_components(package, suitename, arch, session=None):
336     # Check for packages that have moved from one component to another
337     query = """SELECT c.name FROM binaries b, bin_associations ba, suite s, location l, component c, architecture a, files f
338     WHERE b.package=:package AND s.suite_name=:suitename
339       AND (a.arch_string = :arch OR a.arch_string = 'all')
340       AND ba.bin = b.id AND ba.suite = s.id AND b.architecture = a.id
341       AND f.location = l.id
342       AND l.component = c.id
343       AND b.file = f.id"""
344
345     vals = {'package': package, 'suitename': suitename, 'arch': arch}
346
347     return session.execute(query, vals)
348
349 __all__.append('get_binary_components')
350
351 ################################################################################
352
353 class Component(object):
354     def __init__(self, *args, **kwargs):
355         pass
356
357     def __eq__(self, val):
358         if isinstance(val, str):
359             return (self.component_name == val)
360         # This signals to use the normal comparison operator
361         return NotImplemented
362
363     def __ne__(self, val):
364         if isinstance(val, str):
365             return (self.component_name != val)
366         # This signals to use the normal comparison operator
367         return NotImplemented
368
369     def __repr__(self):
370         return '<Component %s>' % self.component_name
371
372
373 __all__.append('Component')
374
375 @session_wrapper
376 def get_component(component, session=None):
377     """
378     Returns database id for given C{component}.
379
380     @type component: string
381     @param component: The name of the override type
382
383     @rtype: int
384     @return: the database id for the given component
385
386     """
387     component = component.lower()
388
389     q = session.query(Component).filter_by(component_name=component)
390
391     try:
392         return q.one()
393     except NoResultFound:
394         return None
395
396 __all__.append('get_component')
397
398 ################################################################################
399
400 class DBConfig(object):
401     def __init__(self, *args, **kwargs):
402         pass
403
404     def __repr__(self):
405         return '<DBConfig %s>' % self.name
406
407 __all__.append('DBConfig')
408
409 ################################################################################
410
411 class ContentFilename(object):
412     def __init__(self, *args, **kwargs):
413         pass
414
415     def __repr__(self):
416         return '<ContentFilename %s>' % self.filename
417
418 __all__.append('ContentFilename')
419
420 def get_or_set_contents_file_id(filename, session=None):
421     """
422     Returns database id for given filename.
423
424     If no matching file is found, a row is inserted.
425
426     @type filename: string
427     @param filename: The filename
428     @type session: SQLAlchemy
429     @param session: Optional SQL session object (a temporary one will be
430     generated if not supplied).  If not passed, a commit will be performed at
431     the end of the function, otherwise the caller is responsible for commiting.
432
433     @rtype: int
434     @return: the database id for the given component
435     """
436     privatetrans = False
437     if session is None:
438         session = DBConn().session()
439         privatetrans = True
440
441     q = session.query(ContentFilename).filter_by(filename=filename)
442
443     try:
444         ret = q.one().cafilename_id
445     except NoResultFound:
446         cf = ContentFilename()
447         cf.filename = filename
448         session.add(cf)
449         if privatetrans:
450             session.commit()
451         else:
452             session.flush()
453         ret = cf.cafilename_id
454
455     if privatetrans:
456         session.close()
457
458     return ret
459
460 __all__.append('get_or_set_contents_file_id')
461
462 @session_wrapper
463 def get_contents(suite, overridetype, section=None, session=None):
464     """
465     Returns contents for a suite / overridetype combination, limiting
466     to a section if not None.
467
468     @type suite: Suite
469     @param suite: Suite object
470
471     @type overridetype: OverrideType
472     @param overridetype: OverrideType object
473
474     @type section: Section
475     @param section: Optional section object to limit results to
476
477     @type session: SQLAlchemy
478     @param session: Optional SQL session object (a temporary one will be
479     generated if not supplied)
480
481     @rtype: ResultsProxy
482     @return: ResultsProxy object set up to return tuples of (filename, section,
483     package, arch_id)
484     """
485
486     # find me all of the contents for a given suite
487     contents_q = """SELECT (p.path||'/'||n.file) AS fn,
488                             s.section,
489                             b.package,
490                             b.architecture
491                    FROM content_associations c join content_file_paths p ON (c.filepath=p.id)
492                    JOIN content_file_names n ON (c.filename=n.id)
493                    JOIN binaries b ON (b.id=c.binary_pkg)
494                    JOIN override o ON (o.package=b.package)
495                    JOIN section s ON (s.id=o.section)
496                    WHERE o.suite = :suiteid AND o.type = :overridetypeid
497                    AND b.type=:overridetypename"""
498
499     vals = {'suiteid': suite.suite_id,
500             'overridetypeid': overridetype.overridetype_id,
501             'overridetypename': overridetype.overridetype}
502
503     if section is not None:
504         contents_q += " AND s.id = :sectionid"
505         vals['sectionid'] = section.section_id
506
507     contents_q += " ORDER BY fn"
508
509     return session.execute(contents_q, vals)
510
511 __all__.append('get_contents')
512
513 ################################################################################
514
515 class ContentFilepath(object):
516     def __init__(self, *args, **kwargs):
517         pass
518
519     def __repr__(self):
520         return '<ContentFilepath %s>' % self.filepath
521
522 __all__.append('ContentFilepath')
523
524 def get_or_set_contents_path_id(filepath, session=None):
525     """
526     Returns database id for given path.
527
528     If no matching file is found, a row is inserted.
529
530     @type filename: string
531     @param filename: The filepath
532     @type session: SQLAlchemy
533     @param session: Optional SQL session object (a temporary one will be
534     generated if not supplied).  If not passed, a commit will be performed at
535     the end of the function, otherwise the caller is responsible for commiting.
536
537     @rtype: int
538     @return: the database id for the given path
539     """
540     privatetrans = False
541     if session is None:
542         session = DBConn().session()
543         privatetrans = True
544
545     q = session.query(ContentFilepath).filter_by(filepath=filepath)
546
547     try:
548         ret = q.one().cafilepath_id
549     except NoResultFound:
550         cf = ContentFilepath()
551         cf.filepath = filepath
552         session.add(cf)
553         if privatetrans:
554             session.commit()
555         else:
556             session.flush()
557         ret = cf.cafilepath_id
558
559     if privatetrans:
560         session.close()
561
562     return ret
563
564 __all__.append('get_or_set_contents_path_id')
565
566 ################################################################################
567
568 class ContentAssociation(object):
569     def __init__(self, *args, **kwargs):
570         pass
571
572     def __repr__(self):
573         return '<ContentAssociation %s>' % self.ca_id
574
575 __all__.append('ContentAssociation')
576
577 def insert_content_paths(binary_id, fullpaths, session=None):
578     """
579     Make sure given path is associated with given binary id
580
581     @type binary_id: int
582     @param binary_id: the id of the binary
583     @type fullpaths: list
584     @param fullpaths: the list of paths of the file being associated with the binary
585     @type session: SQLAlchemy session
586     @param session: Optional SQLAlchemy session.  If this is passed, the caller
587     is responsible for ensuring a transaction has begun and committing the
588     results or rolling back based on the result code.  If not passed, a commit
589     will be performed at the end of the function, otherwise the caller is
590     responsible for commiting.
591
592     @return: True upon success
593     """
594
595     privatetrans = False
596     if session is None:
597         session = DBConn().session()
598         privatetrans = True
599
600     try:
601         # Insert paths
602         pathcache = {}
603         for fullpath in fullpaths:
604             # Get the necessary IDs ...
605             (path, file) = os.path.split(fullpath)
606
607             filepath_id = get_or_set_contents_path_id(path, session)
608             filename_id = get_or_set_contents_file_id(file, session)
609
610             pathcache[fullpath] = (filepath_id, filename_id)
611
612         for fullpath, dat in pathcache.items():
613             ca = ContentAssociation()
614             ca.binary_id = binary_id
615             ca.filepath_id = dat[0]
616             ca.filename_id = dat[1]
617             session.add(ca)
618
619         # Only commit if we set up the session ourself
620         if privatetrans:
621             session.commit()
622             session.close()
623         else:
624             session.flush()
625
626         return True
627
628     except:
629         traceback.print_exc()
630
631         # Only rollback if we set up the session ourself
632         if privatetrans:
633             session.rollback()
634             session.close()
635
636         return False
637
638 __all__.append('insert_content_paths')
639
640 ################################################################################
641
642 class DSCFile(object):
643     def __init__(self, *args, **kwargs):
644         pass
645
646     def __repr__(self):
647         return '<DSCFile %s>' % self.dscfile_id
648
649 __all__.append('DSCFile')
650
651 @session_wrapper
652 def get_dscfiles(dscfile_id=None, source_id=None, poolfile_id=None, session=None):
653     """
654     Returns a list of DSCFiles which may be empty
655
656     @type dscfile_id: int (optional)
657     @param dscfile_id: the dscfile_id of the DSCFiles to find
658
659     @type source_id: int (optional)
660     @param source_id: the source id related to the DSCFiles to find
661
662     @type poolfile_id: int (optional)
663     @param poolfile_id: the poolfile id related to the DSCFiles to find
664
665     @rtype: list
666     @return: Possibly empty list of DSCFiles
667     """
668
669     q = session.query(DSCFile)
670
671     if dscfile_id is not None:
672         q = q.filter_by(dscfile_id=dscfile_id)
673
674     if source_id is not None:
675         q = q.filter_by(source_id=source_id)
676
677     if poolfile_id is not None:
678         q = q.filter_by(poolfile_id=poolfile_id)
679
680     return q.all()
681
682 __all__.append('get_dscfiles')
683
684 ################################################################################
685
686 class PoolFile(object):
687     def __init__(self, *args, **kwargs):
688         pass
689
690     def __repr__(self):
691         return '<PoolFile %s>' % self.filename
692
693 __all__.append('PoolFile')
694
695 @session_wrapper
696 def check_poolfile(filename, filesize, md5sum, location_id, session=None):
697     """
698     Returns a tuple:
699      (ValidFileFound [boolean or None], PoolFile object or None)
700
701     @type filename: string
702     @param filename: the filename of the file to check against the DB
703
704     @type filesize: int
705     @param filesize: the size of the file to check against the DB
706
707     @type md5sum: string
708     @param md5sum: the md5sum of the file to check against the DB
709
710     @type location_id: int
711     @param location_id: the id of the location to look in
712
713     @rtype: tuple
714     @return: Tuple of length 2.
715              If more than one file found with that name:
716                     (None,  None)
717              If valid pool file found: (True, PoolFile object)
718              If valid pool file not found:
719                     (False, None) if no file found
720                     (False, PoolFile object) if file found with size/md5sum mismatch
721     """
722
723     q = session.query(PoolFile).filter_by(filename=filename)
724     q = q.join(Location).filter_by(location_id=location_id)
725
726     ret = None
727
728     if q.count() > 1:
729         ret = (None, None)
730     elif q.count() < 1:
731         ret = (False, None)
732     else:
733         obj = q.one()
734         if obj.md5sum != md5sum or obj.filesize != filesize:
735             ret = (False, obj)
736
737     if ret is None:
738         ret = (True, obj)
739
740     return ret
741
742 __all__.append('check_poolfile')
743
744 @session_wrapper
745 def get_poolfile_by_id(file_id, session=None):
746     """
747     Returns a PoolFile objects or None for the given id
748
749     @type file_id: int
750     @param file_id: the id of the file to look for
751
752     @rtype: PoolFile or None
753     @return: either the PoolFile object or None
754     """
755
756     q = session.query(PoolFile).filter_by(file_id=file_id)
757
758     try:
759         return q.one()
760     except NoResultFound:
761         return None
762
763 __all__.append('get_poolfile_by_id')
764
765
766 @session_wrapper
767 def get_poolfile_by_name(filename, location_id=None, session=None):
768     """
769     Returns an array of PoolFile objects for the given filename and
770     (optionally) location_id
771
772     @type filename: string
773     @param filename: the filename of the file to check against the DB
774
775     @type location_id: int
776     @param location_id: the id of the location to look in (optional)
777
778     @rtype: array
779     @return: array of PoolFile objects
780     """
781
782     q = session.query(PoolFile).filter_by(filename=filename)
783
784     if location_id is not None:
785         q = q.join(Location).filter_by(location_id=location_id)
786
787     return q.all()
788
789 __all__.append('get_poolfile_by_name')
790
791 @session_wrapper
792 def get_poolfile_like_name(filename, session=None):
793     """
794     Returns an array of PoolFile objects which are like the given name
795
796     @type filename: string
797     @param filename: the filename of the file to check against the DB
798
799     @rtype: array
800     @return: array of PoolFile objects
801     """
802
803     # TODO: There must be a way of properly using bind parameters with %FOO%
804     q = session.query(PoolFile).filter(PoolFile.filename.like('%%%s%%' % filename))
805
806     return q.all()
807
808 __all__.append('get_poolfile_like_name')
809
810 ################################################################################
811
812 class Fingerprint(object):
813     def __init__(self, *args, **kwargs):
814         pass
815
816     def __repr__(self):
817         return '<Fingerprint %s>' % self.fingerprint
818
819 __all__.append('Fingerprint')
820
821 def get_or_set_fingerprint(fpr, session=None):
822     """
823     Returns Fingerprint object for given fpr.
824
825     If no matching fpr is found, a row is inserted.
826
827     @type fpr: string
828     @param fpr: The fpr to find / add
829
830     @type session: SQLAlchemy
831     @param session: Optional SQL session object (a temporary one will be
832     generated if not supplied).  If not passed, a commit will be performed at
833     the end of the function, otherwise the caller is responsible for commiting.
834     A flush will be performed either way.
835
836     @rtype: Fingerprint
837     @return: the Fingerprint object for the given fpr
838     """
839     privatetrans = False
840     if session is None:
841         session = DBConn().session()
842         privatetrans = True
843
844     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
845
846     try:
847         ret = q.one()
848     except NoResultFound:
849         fingerprint = Fingerprint()
850         fingerprint.fingerprint = fpr
851         session.add(fingerprint)
852         if privatetrans:
853             session.commit()
854         else:
855             session.flush()
856         ret = fingerprint
857
858     if privatetrans:
859         session.close()
860
861     return ret
862
863 __all__.append('get_or_set_fingerprint')
864
865 ################################################################################
866
867 class Keyring(object):
868     def __init__(self, *args, **kwargs):
869         pass
870
871     def __repr__(self):
872         return '<Keyring %s>' % self.keyring_name
873
874 __all__.append('Keyring')
875
876 def get_or_set_keyring(keyring, session=None):
877     """
878     If C{keyring} does not have an entry in the C{keyrings} table yet, create one
879     and return the new Keyring
880     If C{keyring} already has an entry, simply return the existing Keyring
881
882     @type keyring: string
883     @param keyring: the keyring name
884
885     @rtype: Keyring
886     @return: the Keyring object for this keyring
887
888     """
889     privatetrans = False
890     if session is None:
891         session = DBConn().session()
892         privatetrans = True
893
894     try:
895         obj = session.query(Keyring).filter_by(keyring_name=keyring).first()
896
897         if obj is None:
898             obj = Keyring(keyring_name=keyring)
899             session.add(obj)
900             if privatetrans:
901                 session.commit()
902             else:
903                 session.flush()
904
905         return obj
906     finally:
907         if privatetrans:
908             session.close()
909
910 __all__.append('get_or_set_keyring')
911
912 ################################################################################
913
914 class Location(object):
915     def __init__(self, *args, **kwargs):
916         pass
917
918     def __repr__(self):
919         return '<Location %s (%s)>' % (self.path, self.location_id)
920
921 __all__.append('Location')
922
923 @session_wrapper
924 def get_location(location, component=None, archive=None, session=None):
925     """
926     Returns Location object for the given combination of location, component
927     and archive
928
929     @type location: string
930     @param location: the path of the location, e.g. I{/srv/ftp.debian.org/ftp/pool/}
931
932     @type component: string
933     @param component: the component name (if None, no restriction applied)
934
935     @type archive: string
936     @param archive_id: the archive name (if None, no restriction applied)
937
938     @rtype: Location / None
939     @return: Either a Location object or None if one can't be found
940     """
941
942     q = session.query(Location).filter_by(path=location)
943
944     if archive is not None:
945         q = q.join(Archive).filter_by(archive_name=archive)
946
947     if component is not None:
948         q = q.join(Component).filter_by(component_name=component)
949
950     try:
951         return q.one()
952     except NoResultFound:
953         return None
954
955 __all__.append('get_location')
956
957 ################################################################################
958
959 class Maintainer(object):
960     def __init__(self, *args, **kwargs):
961         pass
962
963     def __repr__(self):
964         return '''<Maintainer '%s' (%s)>''' % (self.name, self.maintainer_id)
965
966     def get_split_maintainer(self):
967         if not hasattr(self, 'name') or self.name is None:
968             return ('', '', '', '')
969
970         return fix_maintainer(self.name.strip())
971
972 __all__.append('Maintainer')
973
974 def get_or_set_maintainer(name, session=None):
975     """
976     Returns Maintainer object for given maintainer name.
977
978     If no matching maintainer name is found, a row is inserted.
979
980     @type name: string
981     @param name: The maintainer name to add
982
983     @type session: SQLAlchemy
984     @param session: Optional SQL session object (a temporary one will be
985     generated if not supplied).  If not passed, a commit will be performed at
986     the end of the function, otherwise the caller is responsible for commiting.
987     A flush will be performed either way.
988
989     @rtype: Maintainer
990     @return: the Maintainer object for the given maintainer
991     """
992     privatetrans = False
993     if session is None:
994         session = DBConn().session()
995         privatetrans = True
996
997     q = session.query(Maintainer).filter_by(name=name)
998     try:
999         ret = q.one()
1000     except NoResultFound:
1001         maintainer = Maintainer()
1002         maintainer.name = name
1003         session.add(maintainer)
1004         if privatetrans:
1005             session.commit()
1006         else:
1007             session.flush()
1008         ret = maintainer
1009
1010     if privatetrans:
1011         session.close()
1012
1013     return ret
1014
1015 __all__.append('get_or_set_maintainer')
1016
1017 def get_maintainer(maintainer_id, session=None):
1018     """
1019     Return the name of the maintainer behind C{maintainer_id} or None if that
1020     maintainer_id is invalid.
1021
1022     @type maintainer_id: int
1023     @param maintainer_id: the id of the maintainer
1024
1025     @rtype: Maintainer
1026     @return: the Maintainer with this C{maintainer_id}
1027     """
1028
1029     privatetrans = False
1030     if session is None:
1031         session = DBConn().session()
1032         privatetrans = True
1033
1034     try:
1035         return session.query(Maintainer).get(maintainer_id)
1036     finally:
1037         if privatetrans:
1038             session.close()
1039
1040 __all__.append('get_maintainer')
1041
1042 ################################################################################
1043
1044 class NewComment(object):
1045     def __init__(self, *args, **kwargs):
1046         pass
1047
1048     def __repr__(self):
1049         return '''<NewComment for '%s %s' (%s)>''' % (self.package, self.version, self.comment_id)
1050
1051 __all__.append('NewComment')
1052
1053 @session_wrapper
1054 def has_new_comment(package, version, session=None):
1055     """
1056     Returns true if the given combination of C{package}, C{version} has a comment.
1057
1058     @type package: string
1059     @param package: name of the package
1060
1061     @type version: string
1062     @param version: package version
1063
1064     @type session: Session
1065     @param session: Optional SQLA session object (a temporary one will be
1066     generated if not supplied)
1067
1068     @rtype: boolean
1069     @return: true/false
1070     """
1071
1072     q = session.query(NewComment)
1073     q = q.filter_by(package=package)
1074     q = q.filter_by(version=version)
1075
1076     return bool(q.count() > 0)
1077
1078 __all__.append('has_new_comment')
1079
1080 @session_wrapper
1081 def get_new_comments(package=None, version=None, comment_id=None, session=None):
1082     """
1083     Returns (possibly empty) list of NewComment objects for the given
1084     parameters
1085
1086     @type package: string (optional)
1087     @param package: name of the package
1088
1089     @type version: string (optional)
1090     @param version: package version
1091
1092     @type comment_id: int (optional)
1093     @param comment_id: An id of a comment
1094
1095     @type session: Session
1096     @param session: Optional SQLA session object (a temporary one will be
1097     generated if not supplied)
1098
1099     @rtype: list
1100     @return: A (possibly empty) list of NewComment objects will be returned
1101     """
1102
1103     q = session.query(NewComment)
1104     if package is not None: q = q.filter_by(package=package)
1105     if version is not None: q = q.filter_by(version=version)
1106     if comment_id is not None: q = q.filter_by(comment_id=comment_id)
1107
1108     return q.all()
1109
1110 __all__.append('get_new_comments')
1111
1112 ################################################################################
1113
1114 class Override(object):
1115     def __init__(self, *args, **kwargs):
1116         pass
1117
1118     def __repr__(self):
1119         return '<Override %s (%s)>' % (self.package, self.suite_id)
1120
1121 __all__.append('Override')
1122
1123 @session_wrapper
1124 def get_override(package, suite=None, component=None, overridetype=None, session=None):
1125     """
1126     Returns Override object for the given parameters
1127
1128     @type package: string
1129     @param package: The name of the package
1130
1131     @type suite: string, list or None
1132     @param suite: The name of the suite (or suites if a list) to limit to.  If
1133                   None, don't limit.  Defaults to None.
1134
1135     @type component: string, list or None
1136     @param component: The name of the component (or components if a list) to
1137                       limit to.  If None, don't limit.  Defaults to None.
1138
1139     @type overridetype: string, list or None
1140     @param overridetype: The name of the overridetype (or overridetypes if a list) to
1141                          limit to.  If None, don't limit.  Defaults to None.
1142
1143     @type session: Session
1144     @param session: Optional SQLA session object (a temporary one will be
1145     generated if not supplied)
1146
1147     @rtype: list
1148     @return: A (possibly empty) list of Override objects will be returned
1149     """
1150
1151     q = session.query(Override)
1152     q = q.filter_by(package=package)
1153
1154     if suite is not None:
1155         if not isinstance(suite, list): suite = [suite]
1156         q = q.join(Suite).filter(Suite.suite_name.in_(suite))
1157
1158     if component is not None:
1159         if not isinstance(component, list): component = [component]
1160         q = q.join(Component).filter(Component.component_name.in_(component))
1161
1162     if overridetype is not None:
1163         if not isinstance(overridetype, list): overridetype = [overridetype]
1164         q = q.join(OverrideType).filter(OverrideType.overridetype.in_(overridetype))
1165
1166     return q.all()
1167
1168 __all__.append('get_override')
1169
1170
1171 ################################################################################
1172
1173 class OverrideType(object):
1174     def __init__(self, *args, **kwargs):
1175         pass
1176
1177     def __repr__(self):
1178         return '<OverrideType %s>' % self.overridetype
1179
1180 __all__.append('OverrideType')
1181
1182 @session_wrapper
1183 def get_override_type(override_type, session=None):
1184     """
1185     Returns OverrideType object for given C{override type}.
1186
1187     @type override_type: string
1188     @param override_type: The name of the override type
1189
1190     @type session: Session
1191     @param session: Optional SQLA session object (a temporary one will be
1192     generated if not supplied)
1193
1194     @rtype: int
1195     @return: the database id for the given override type
1196     """
1197
1198     q = session.query(OverrideType).filter_by(overridetype=override_type)
1199
1200     try:
1201         return q.one()
1202     except NoResultFound:
1203         return None
1204
1205 __all__.append('get_override_type')
1206
1207 ################################################################################
1208
1209 class PendingContentAssociation(object):
1210     def __init__(self, *args, **kwargs):
1211         pass
1212
1213     def __repr__(self):
1214         return '<PendingContentAssociation %s>' % self.pca_id
1215
1216 __all__.append('PendingContentAssociation')
1217
1218 def insert_pending_content_paths(package, fullpaths, session=None):
1219     """
1220     Make sure given paths are temporarily associated with given
1221     package
1222
1223     @type package: dict
1224     @param package: the package to associate with should have been read in from the binary control file
1225     @type fullpaths: list
1226     @param fullpaths: the list of paths of the file being associated with the binary
1227     @type session: SQLAlchemy session
1228     @param session: Optional SQLAlchemy session.  If this is passed, the caller
1229     is responsible for ensuring a transaction has begun and committing the
1230     results or rolling back based on the result code.  If not passed, a commit
1231     will be performed at the end of the function
1232
1233     @return: True upon success, False if there is a problem
1234     """
1235
1236     privatetrans = False
1237
1238     if session is None:
1239         session = DBConn().session()
1240         privatetrans = True
1241
1242     try:
1243         arch = get_architecture(package['Architecture'], session)
1244         arch_id = arch.arch_id
1245
1246         # Remove any already existing recorded files for this package
1247         q = session.query(PendingContentAssociation)
1248         q = q.filter_by(package=package['Package'])
1249         q = q.filter_by(version=package['Version'])
1250         q = q.filter_by(architecture=arch_id)
1251         q.delete()
1252
1253         # Insert paths
1254         pathcache = {}
1255         for fullpath in fullpaths:
1256             (path, file) = os.path.split(fullpath)
1257
1258             if path.startswith( "./" ):
1259                 path = path[2:]
1260
1261             filepath_id = get_or_set_contents_path_id(path, session)
1262             filename_id = get_or_set_contents_file_id(file, session)
1263
1264             pathcache[fullpath] = (filepath_id, filename_id)
1265
1266         for fullpath, dat in pathcache.items():
1267             pca = PendingContentAssociation()
1268             pca.package = package['Package']
1269             pca.version = package['Version']
1270             pca.filepath_id = dat[0]
1271             pca.filename_id = dat[1]
1272             pca.architecture = arch_id
1273             session.add(pca)
1274
1275         # Only commit if we set up the session ourself
1276         if privatetrans:
1277             session.commit()
1278             session.close()
1279         else:
1280             session.flush()
1281
1282         return True
1283     except Exception, e:
1284         traceback.print_exc()
1285
1286         # Only rollback if we set up the session ourself
1287         if privatetrans:
1288             session.rollback()
1289             session.close()
1290
1291         return False
1292
1293 __all__.append('insert_pending_content_paths')
1294
1295 ################################################################################
1296
1297 class Priority(object):
1298     def __init__(self, *args, **kwargs):
1299         pass
1300
1301     def __eq__(self, val):
1302         if isinstance(val, str):
1303             return (self.priority == val)
1304         # This signals to use the normal comparison operator
1305         return NotImplemented
1306
1307     def __ne__(self, val):
1308         if isinstance(val, str):
1309             return (self.priority != val)
1310         # This signals to use the normal comparison operator
1311         return NotImplemented
1312
1313     def __repr__(self):
1314         return '<Priority %s (%s)>' % (self.priority, self.priority_id)
1315
1316 __all__.append('Priority')
1317
1318 @session_wrapper
1319 def get_priority(priority, session=None):
1320     """
1321     Returns Priority object for given C{priority name}.
1322
1323     @type priority: string
1324     @param priority: The name of the priority
1325
1326     @type session: Session
1327     @param session: Optional SQLA session object (a temporary one will be
1328     generated if not supplied)
1329
1330     @rtype: Priority
1331     @return: Priority object for the given priority
1332     """
1333
1334     q = session.query(Priority).filter_by(priority=priority)
1335
1336     try:
1337         return q.one()
1338     except NoResultFound:
1339         return None
1340
1341 __all__.append('get_priority')
1342
1343 @session_wrapper
1344 def get_priorities(session=None):
1345     """
1346     Returns dictionary of priority names -> id mappings
1347
1348     @type session: Session
1349     @param session: Optional SQL session object (a temporary one will be
1350     generated if not supplied)
1351
1352     @rtype: dictionary
1353     @return: dictionary of priority names -> id mappings
1354     """
1355
1356     ret = {}
1357     q = session.query(Priority)
1358     for x in q.all():
1359         ret[x.priority] = x.priority_id
1360
1361     return ret
1362
1363 __all__.append('get_priorities')
1364
1365 ################################################################################
1366
1367 class Queue(object):
1368     def __init__(self, *args, **kwargs):
1369         pass
1370
1371     def __repr__(self):
1372         return '<Queue %s>' % self.queue_name
1373
1374     def autobuild_upload(self, changes, srcpath, session=None):
1375         """
1376         Update queue_build database table used for incoming autobuild support.
1377
1378         @type changes: Changes
1379         @param changes: changes object for the upload to process
1380
1381         @type srcpath: string
1382         @param srcpath: path for the queue file entries/link destinations
1383
1384         @type session: SQLAlchemy session
1385         @param session: Optional SQLAlchemy session.  If this is passed, the
1386         caller is responsible for ensuring a transaction has begun and
1387         committing the results or rolling back based on the result code.  If
1388         not passed, a commit will be performed at the end of the function,
1389         otherwise the caller is responsible for commiting.
1390
1391         @rtype: NoneType or string
1392         @return: None if the operation failed, a string describing the error if not
1393         """
1394
1395         privatetrans = False
1396         if session is None:
1397             session = DBConn().session()
1398             privatetrans = True
1399
1400         # TODO: Remove by moving queue config into the database
1401         conf = Config()
1402
1403         for suitename in changes.changes["distribution"].keys():
1404             # TODO: Move into database as:
1405             #       buildqueuedir TEXT DEFAULT NULL (i.e. NULL is no build)
1406             #       buildqueuecopy BOOLEAN NOT NULL DEFAULT FALSE (i.e. default is symlink)
1407             #       This also gets rid of the SecurityQueueBuild hack below
1408             if suitename not in conf.ValueList("Dinstall::QueueBuildSuites"):
1409                 continue
1410
1411             # Find suite object
1412             s = get_suite(suitename, session)
1413             if s is None:
1414                 return "INTERNAL ERROR: Could not find suite %s" % suitename
1415
1416             # TODO: Get from database as above
1417             dest_dir = conf["Dir::QueueBuild"]
1418
1419             # TODO: Move into database as above
1420             if conf.FindB("Dinstall::SecurityQueueBuild"):
1421                 dest_dir = os.path.join(dest_dir, suitename)
1422
1423             for file_entry in changes.files.keys():
1424                 src = os.path.join(srcpath, file_entry)
1425                 dest = os.path.join(dest_dir, file_entry)
1426
1427                 # TODO: Move into database as above
1428                 if conf.FindB("Dinstall::SecurityQueueBuild"):
1429                     # Copy it since the original won't be readable by www-data
1430                     utils.copy(src, dest)
1431                 else:
1432                     # Create a symlink to it
1433                     os.symlink(src, dest)
1434
1435                 qb = QueueBuild()
1436                 qb.suite_id = s.suite_id
1437                 qb.queue_id = self.queue_id
1438                 qb.filename = dest
1439                 qb.in_queue = True
1440
1441                 session.add(qb)
1442
1443             # If the .orig.tar.gz is in the pool, create a symlink to
1444             # it (if one doesn't already exist)
1445             if changes.orig_tar_id:
1446                 # Determine the .orig.tar.gz file name
1447                 for dsc_file in changes.dsc_files.keys():
1448                     if dsc_file.endswith(".orig.tar.gz"):
1449                         filename = dsc_file
1450
1451                 dest = os.path.join(dest_dir, filename)
1452
1453                 # If it doesn't exist, create a symlink
1454                 if not os.path.exists(dest):
1455                     q = session.execute("SELECT l.path, f.filename FROM location l, files f WHERE f.id = :id and f.location = l.id",
1456                                         {'id': changes.orig_tar_id})
1457                     res = q.fetchone()
1458                     if not res:
1459                         return "[INTERNAL ERROR] Couldn't find id %s in files table." % (changes.orig_tar_id)
1460
1461                     src = os.path.join(res[0], res[1])
1462                     os.symlink(src, dest)
1463
1464                     # Add it to the list of packages for later processing by apt-ftparchive
1465                     qb = QueueBuild()
1466                     qb.suite_id = s.suite_id
1467                     qb.queue_id = self.queue_id
1468                     qb.filename = dest
1469                     qb.in_queue = True
1470                     session.add(qb)
1471
1472                 # If it does, update things to ensure it's not removed prematurely
1473                 else:
1474                     qb = get_queue_build(dest, s.suite_id, session)
1475                     if qb is None:
1476                         qb.in_queue = True
1477                         qb.last_used = None
1478                         session.add(qb)
1479
1480         if privatetrans:
1481             session.commit()
1482             session.close()
1483
1484         return None
1485
1486 __all__.append('Queue')
1487
1488 @session_wrapper
1489 def get_queue(queuename, session=None):
1490     """
1491     Returns Queue object for given C{queue name}.
1492
1493     @type queuename: string
1494     @param queuename: The name of the queue
1495
1496     @type session: Session
1497     @param session: Optional SQLA session object (a temporary one will be
1498     generated if not supplied)
1499
1500     @rtype: Queue
1501     @return: Queue object for the given queue
1502     """
1503
1504     q = session.query(Queue).filter_by(queue_name=queuename)
1505
1506     try:
1507         return q.one()
1508     except NoResultFound:
1509         return None
1510
1511 __all__.append('get_queue')
1512
1513 ################################################################################
1514
1515 class QueueBuild(object):
1516     def __init__(self, *args, **kwargs):
1517         pass
1518
1519     def __repr__(self):
1520         return '<QueueBuild %s (%s)>' % (self.filename, self.queue_id)
1521
1522 __all__.append('QueueBuild')
1523
1524 @session_wrapper
1525 def get_queue_build(filename, suite, session=None):
1526     """
1527     Returns QueueBuild object for given C{filename} and C{suite}.
1528
1529     @type filename: string
1530     @param filename: The name of the file
1531
1532     @type suiteid: int or str
1533     @param suiteid: Suite name or ID
1534
1535     @type session: Session
1536     @param session: Optional SQLA session object (a temporary one will be
1537     generated if not supplied)
1538
1539     @rtype: Queue
1540     @return: Queue object for the given queue
1541     """
1542
1543     if isinstance(suite, int):
1544         q = session.query(QueueBuild).filter_by(filename=filename).filter_by(suite_id=suite)
1545     else:
1546         q = session.query(QueueBuild).filter_by(filename=filename)
1547         q = q.join(Suite).filter_by(suite_name=suite)
1548
1549     try:
1550         return q.one()
1551     except NoResultFound:
1552         return None
1553
1554 __all__.append('get_queue_build')
1555
1556 ################################################################################
1557
1558 class Section(object):
1559     def __init__(self, *args, **kwargs):
1560         pass
1561
1562     def __eq__(self, val):
1563         if isinstance(val, str):
1564             return (self.section == val)
1565         # This signals to use the normal comparison operator
1566         return NotImplemented
1567
1568     def __ne__(self, val):
1569         if isinstance(val, str):
1570             return (self.section != val)
1571         # This signals to use the normal comparison operator
1572         return NotImplemented
1573
1574     def __repr__(self):
1575         return '<Section %s>' % self.section
1576
1577 __all__.append('Section')
1578
1579 @session_wrapper
1580 def get_section(section, session=None):
1581     """
1582     Returns Section object for given C{section name}.
1583
1584     @type section: string
1585     @param section: The name of the section
1586
1587     @type session: Session
1588     @param session: Optional SQLA session object (a temporary one will be
1589     generated if not supplied)
1590
1591     @rtype: Section
1592     @return: Section object for the given section name
1593     """
1594
1595     q = session.query(Section).filter_by(section=section)
1596
1597     try:
1598         return q.one()
1599     except NoResultFound:
1600         return None
1601
1602 __all__.append('get_section')
1603
1604 @session_wrapper
1605 def get_sections(session=None):
1606     """
1607     Returns dictionary of section names -> id mappings
1608
1609     @type session: Session
1610     @param session: Optional SQL session object (a temporary one will be
1611     generated if not supplied)
1612
1613     @rtype: dictionary
1614     @return: dictionary of section names -> id mappings
1615     """
1616
1617     ret = {}
1618     q = session.query(Section)
1619     for x in q.all():
1620         ret[x.section] = x.section_id
1621
1622     return ret
1623
1624 __all__.append('get_sections')
1625
1626 ################################################################################
1627
1628 class DBSource(object):
1629     def __init__(self, *args, **kwargs):
1630         pass
1631
1632     def __repr__(self):
1633         return '<DBSource %s (%s)>' % (self.source, self.version)
1634
1635 __all__.append('DBSource')
1636
1637 @session_wrapper
1638 def source_exists(source, source_version, suites = ["any"], session=None):
1639     """
1640     Ensure that source exists somewhere in the archive for the binary
1641     upload being processed.
1642       1. exact match     => 1.0-3
1643       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
1644
1645     @type package: string
1646     @param package: package source name
1647
1648     @type source_version: string
1649     @param source_version: expected source version
1650
1651     @type suites: list
1652     @param suites: list of suites to check in, default I{any}
1653
1654     @type session: Session
1655     @param session: Optional SQLA session object (a temporary one will be
1656     generated if not supplied)
1657
1658     @rtype: int
1659     @return: returns 1 if a source with expected version is found, otherwise 0
1660
1661     """
1662
1663     cnf = Config()
1664     ret = 1
1665
1666     for suite in suites:
1667         q = session.query(DBSource).filter_by(source=source)
1668         if suite != "any":
1669             # source must exist in suite X, or in some other suite that's
1670             # mapped to X, recursively... silent-maps are counted too,
1671             # unreleased-maps aren't.
1672             maps = cnf.ValueList("SuiteMappings")[:]
1673             maps.reverse()
1674             maps = [ m.split() for m in maps ]
1675             maps = [ (x[1], x[2]) for x in maps
1676                             if x[0] == "map" or x[0] == "silent-map" ]
1677             s = [suite]
1678             for x in maps:
1679                 if x[1] in s and x[0] not in s:
1680                     s.append(x[0])
1681
1682             q = q.join(SrcAssociation).join(Suite)
1683             q = q.filter(Suite.suite_name.in_(s))
1684
1685         # Reduce the query results to a list of version numbers
1686         ql = [ j.version for j in q.all() ]
1687
1688         # Try (1)
1689         if source_version in ql:
1690             continue
1691
1692         # Try (2)
1693         from daklib.regexes import re_bin_only_nmu
1694         orig_source_version = re_bin_only_nmu.sub('', source_version)
1695         if orig_source_version in ql:
1696             continue
1697
1698         # No source found so return not ok
1699         ret = 0
1700
1701     return ret
1702
1703 __all__.append('source_exists')
1704
1705 @session_wrapper
1706 def get_suites_source_in(source, session=None):
1707     """
1708     Returns list of Suite objects which given C{source} name is in
1709
1710     @type source: str
1711     @param source: DBSource package name to search for
1712
1713     @rtype: list
1714     @return: list of Suite objects for the given source
1715     """
1716
1717     return session.query(Suite).join(SrcAssociation).join(DBSource).filter_by(source=source).all()
1718
1719 __all__.append('get_suites_source_in')
1720
1721 @session_wrapper
1722 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
1723     """
1724     Returns list of DBSource objects for given C{source} name and other parameters
1725
1726     @type source: str
1727     @param source: DBSource package name to search for
1728
1729     @type source: str or None
1730     @param source: DBSource version name to search for or None if not applicable
1731
1732     @type dm_upload_allowed: bool
1733     @param dm_upload_allowed: If None, no effect.  If True or False, only
1734     return packages with that dm_upload_allowed setting
1735
1736     @type session: Session
1737     @param session: Optional SQL session object (a temporary one will be
1738     generated if not supplied)
1739
1740     @rtype: list
1741     @return: list of DBSource objects for the given name (may be empty)
1742     """
1743
1744     q = session.query(DBSource).filter_by(source=source)
1745
1746     if version is not None:
1747         q = q.filter_by(version=version)
1748
1749     if dm_upload_allowed is not None:
1750         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
1751
1752     return q.all()
1753
1754 __all__.append('get_sources_from_name')
1755
1756 @session_wrapper
1757 def get_source_in_suite(source, suite, session=None):
1758     """
1759     Returns list of DBSource objects for a combination of C{source} and C{suite}.
1760
1761       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
1762       - B{suite} - a suite name, eg. I{unstable}
1763
1764     @type source: string
1765     @param source: source package name
1766
1767     @type suite: string
1768     @param suite: the suite name
1769
1770     @rtype: string
1771     @return: the version for I{source} in I{suite}
1772
1773     """
1774
1775     q = session.query(SrcAssociation)
1776     q = q.join('source').filter_by(source=source)
1777     q = q.join('suite').filter_by(suite_name=suite)
1778
1779     try:
1780         return q.one().source
1781     except NoResultFound:
1782         return None
1783
1784 __all__.append('get_source_in_suite')
1785
1786 ################################################################################
1787
1788 class SrcAssociation(object):
1789     def __init__(self, *args, **kwargs):
1790         pass
1791
1792     def __repr__(self):
1793         return '<SrcAssociation %s (%s, %s)>' % (self.sa_id, self.source, self.suite)
1794
1795 __all__.append('SrcAssociation')
1796
1797 ################################################################################
1798
1799 class SrcUploader(object):
1800     def __init__(self, *args, **kwargs):
1801         pass
1802
1803     def __repr__(self):
1804         return '<SrcUploader %s>' % self.uploader_id
1805
1806 __all__.append('SrcUploader')
1807
1808 ################################################################################
1809
1810 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
1811                  ('SuiteID', 'suite_id'),
1812                  ('Version', 'version'),
1813                  ('Origin', 'origin'),
1814                  ('Label', 'label'),
1815                  ('Description', 'description'),
1816                  ('Untouchable', 'untouchable'),
1817                  ('Announce', 'announce'),
1818                  ('Codename', 'codename'),
1819                  ('OverrideCodename', 'overridecodename'),
1820                  ('ValidTime', 'validtime'),
1821                  ('Priority', 'priority'),
1822                  ('NotAutomatic', 'notautomatic'),
1823                  ('CopyChanges', 'copychanges'),
1824                  ('CopyDotDak', 'copydotdak'),
1825                  ('CommentsDir', 'commentsdir'),
1826                  ('OverrideSuite', 'overridesuite'),
1827                  ('ChangelogBase', 'changelogbase')]
1828
1829
1830 class Suite(object):
1831     def __init__(self, *args, **kwargs):
1832         pass
1833
1834     def __repr__(self):
1835         return '<Suite %s>' % self.suite_name
1836
1837     def __eq__(self, val):
1838         if isinstance(val, str):
1839             return (self.suite_name == val)
1840         # This signals to use the normal comparison operator
1841         return NotImplemented
1842
1843     def __ne__(self, val):
1844         if isinstance(val, str):
1845             return (self.suite_name != val)
1846         # This signals to use the normal comparison operator
1847         return NotImplemented
1848
1849     def details(self):
1850         ret = []
1851         for disp, field in SUITE_FIELDS:
1852             val = getattr(self, field, None)
1853             if val is not None:
1854                 ret.append("%s: %s" % (disp, val))
1855
1856         return "\n".join(ret)
1857
1858 __all__.append('Suite')
1859
1860 @session_wrapper
1861 def get_suite_architecture(suite, architecture, session=None):
1862     """
1863     Returns a SuiteArchitecture object given C{suite} and ${arch} or None if it
1864     doesn't exist
1865
1866     @type suite: str
1867     @param suite: Suite name to search for
1868
1869     @type architecture: str
1870     @param architecture: Architecture name to search for
1871
1872     @type session: Session
1873     @param session: Optional SQL session object (a temporary one will be
1874     generated if not supplied)
1875
1876     @rtype: SuiteArchitecture
1877     @return: the SuiteArchitecture object or None
1878     """
1879
1880     q = session.query(SuiteArchitecture)
1881     q = q.join(Architecture).filter_by(arch_string=architecture)
1882     q = q.join(Suite).filter_by(suite_name=suite)
1883
1884     try:
1885         return q.one()
1886     except NoResultFound:
1887         return None
1888
1889 __all__.append('get_suite_architecture')
1890
1891 @session_wrapper
1892 def get_suite(suite, session=None):
1893     """
1894     Returns Suite object for given C{suite name}.
1895
1896     @type suite: string
1897     @param suite: The name of the suite
1898
1899     @type session: Session
1900     @param session: Optional SQLA session object (a temporary one will be
1901     generated if not supplied)
1902
1903     @rtype: Suite
1904     @return: Suite object for the requested suite name (None if not presenT)
1905     """
1906
1907     q = session.query(Suite).filter_by(suite_name=suite)
1908
1909     try:
1910         return q.one()
1911     except NoResultFound:
1912         return None
1913
1914 __all__.append('get_suite')
1915
1916 ################################################################################
1917
1918 class SuiteArchitecture(object):
1919     def __init__(self, *args, **kwargs):
1920         pass
1921
1922     def __repr__(self):
1923         return '<SuiteArchitecture (%s, %s)>' % (self.suite_id, self.arch_id)
1924
1925 __all__.append('SuiteArchitecture')
1926
1927 @session_wrapper
1928 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
1929     """
1930     Returns list of Architecture objects for given C{suite} name
1931
1932     @type source: str
1933     @param source: Suite name to search for
1934
1935     @type skipsrc: boolean
1936     @param skipsrc: Whether to skip returning the 'source' architecture entry
1937     (Default False)
1938
1939     @type skipall: boolean
1940     @param skipall: Whether to skip returning the 'all' architecture entry
1941     (Default False)
1942
1943     @type session: Session
1944     @param session: Optional SQL session object (a temporary one will be
1945     generated if not supplied)
1946
1947     @rtype: list
1948     @return: list of Architecture objects for the given name (may be empty)
1949     """
1950
1951     q = session.query(Architecture)
1952     q = q.join(SuiteArchitecture)
1953     q = q.join(Suite).filter_by(suite_name=suite)
1954
1955     if skipsrc:
1956         q = q.filter(Architecture.arch_string != 'source')
1957
1958     if skipall:
1959         q = q.filter(Architecture.arch_string != 'all')
1960
1961     q = q.order_by('arch_string')
1962
1963     return q.all()
1964
1965 __all__.append('get_suite_architectures')
1966
1967 ################################################################################
1968
1969 class Uid(object):
1970     def __init__(self, *args, **kwargs):
1971         pass
1972
1973     def __eq__(self, val):
1974         if isinstance(val, str):
1975             return (self.uid == val)
1976         # This signals to use the normal comparison operator
1977         return NotImplemented
1978
1979     def __ne__(self, val):
1980         if isinstance(val, str):
1981             return (self.uid != val)
1982         # This signals to use the normal comparison operator
1983         return NotImplemented
1984
1985     def __repr__(self):
1986         return '<Uid %s (%s)>' % (self.uid, self.name)
1987
1988 __all__.append('Uid')
1989
1990 def add_database_user(uidname, session=None):
1991     """
1992     Adds a database user
1993
1994     @type uidname: string
1995     @param uidname: The uid of the user to add
1996
1997     @type session: SQLAlchemy
1998     @param session: Optional SQL session object (a temporary one will be
1999     generated if not supplied).  If not passed, a commit will be performed at
2000     the end of the function, otherwise the caller is responsible for commiting.
2001
2002     @rtype: Uid
2003     @return: the uid object for the given uidname
2004     """
2005
2006     privatetrans = False
2007     if session is None:
2008         session = DBConn().session()
2009         privatetrans = True
2010
2011     session.execute("CREATE USER :uid", {'uid': uidname})
2012
2013     if privatetrans:
2014         session.commit()
2015         session.close()
2016
2017 __all__.append('add_database_user')
2018
2019 def get_or_set_uid(uidname, session=None):
2020     """
2021     Returns uid object for given uidname.
2022
2023     If no matching uidname is found, a row is inserted.
2024
2025     @type uidname: string
2026     @param uidname: The uid to add
2027
2028     @type session: SQLAlchemy
2029     @param session: Optional SQL session object (a temporary one will be
2030     generated if not supplied).  If not passed, a commit will be performed at
2031     the end of the function, otherwise the caller is responsible for commiting.
2032
2033     @rtype: Uid
2034     @return: the uid object for the given uidname
2035     """
2036
2037     privatetrans = False
2038     if session is None:
2039         session = DBConn().session()
2040         privatetrans = True
2041
2042     q = session.query(Uid).filter_by(uid=uidname)
2043
2044     try:
2045         ret = q.one()
2046     except NoResultFound:
2047         uid = Uid()
2048         uid.uid = uidname
2049         session.add(uid)
2050         if privatetrans:
2051             session.commit()
2052         else:
2053             session.flush()
2054         ret = uid
2055
2056     if privatetrans:
2057         session.close()
2058
2059     return ret
2060
2061 __all__.append('get_or_set_uid')
2062
2063 @session_wrapper
2064 def get_uid_from_fingerprint(fpr, session=None):
2065     q = session.query(Uid)
2066     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
2067
2068     try:
2069         return q.one()
2070     except NoResultFound:
2071         return None
2072
2073 __all__.append('get_uid_from_fingerprint')
2074
2075 ################################################################################
2076
2077 class DBConn(Singleton):
2078     """
2079     database module init.
2080     """
2081     def __init__(self, *args, **kwargs):
2082         super(DBConn, self).__init__(*args, **kwargs)
2083
2084     def _startup(self, *args, **kwargs):
2085         self.debug = False
2086         if kwargs.has_key('debug'):
2087             self.debug = True
2088         self.__createconn()
2089
2090     def __setuptables(self):
2091         self.tbl_architecture = Table('architecture', self.db_meta, autoload=True)
2092         self.tbl_archive = Table('archive', self.db_meta, autoload=True)
2093         self.tbl_bin_associations = Table('bin_associations', self.db_meta, autoload=True)
2094         self.tbl_binaries = Table('binaries', self.db_meta, autoload=True)
2095         self.tbl_component = Table('component', self.db_meta, autoload=True)
2096         self.tbl_config = Table('config', self.db_meta, autoload=True)
2097         self.tbl_content_associations = Table('content_associations', self.db_meta, autoload=True)
2098         self.tbl_content_file_names = Table('content_file_names', self.db_meta, autoload=True)
2099         self.tbl_content_file_paths = Table('content_file_paths', self.db_meta, autoload=True)
2100         self.tbl_dsc_files = Table('dsc_files', self.db_meta, autoload=True)
2101         self.tbl_files = Table('files', self.db_meta, autoload=True)
2102         self.tbl_fingerprint = Table('fingerprint', self.db_meta, autoload=True)
2103         self.tbl_keyrings = Table('keyrings', self.db_meta, autoload=True)
2104         self.tbl_location = Table('location', self.db_meta, autoload=True)
2105         self.tbl_maintainer = Table('maintainer', self.db_meta, autoload=True)
2106         self.tbl_new_comments = Table('new_comments', self.db_meta, autoload=True)
2107         self.tbl_override = Table('override', self.db_meta, autoload=True)
2108         self.tbl_override_type = Table('override_type', self.db_meta, autoload=True)
2109         self.tbl_pending_content_associations = Table('pending_content_associations', self.db_meta, autoload=True)
2110         self.tbl_priority = Table('priority', self.db_meta, autoload=True)
2111         self.tbl_queue = Table('queue', self.db_meta, autoload=True)
2112         self.tbl_queue_build = Table('queue_build', self.db_meta, autoload=True)
2113         self.tbl_section = Table('section', self.db_meta, autoload=True)
2114         self.tbl_source = Table('source', self.db_meta, autoload=True)
2115         self.tbl_src_associations = Table('src_associations', self.db_meta, autoload=True)
2116         self.tbl_src_uploaders = Table('src_uploaders', self.db_meta, autoload=True)
2117         self.tbl_suite = Table('suite', self.db_meta, autoload=True)
2118         self.tbl_suite_architectures = Table('suite_architectures', self.db_meta, autoload=True)
2119         self.tbl_uid = Table('uid', self.db_meta, autoload=True)
2120
2121     def __setupmappers(self):
2122         mapper(Architecture, self.tbl_architecture,
2123                properties = dict(arch_id = self.tbl_architecture.c.id))
2124
2125         mapper(Archive, self.tbl_archive,
2126                properties = dict(archive_id = self.tbl_archive.c.id,
2127                                  archive_name = self.tbl_archive.c.name))
2128
2129         mapper(BinAssociation, self.tbl_bin_associations,
2130                properties = dict(ba_id = self.tbl_bin_associations.c.id,
2131                                  suite_id = self.tbl_bin_associations.c.suite,
2132                                  suite = relation(Suite),
2133                                  binary_id = self.tbl_bin_associations.c.bin,
2134                                  binary = relation(DBBinary)))
2135
2136         mapper(DBBinary, self.tbl_binaries,
2137                properties = dict(binary_id = self.tbl_binaries.c.id,
2138                                  package = self.tbl_binaries.c.package,
2139                                  version = self.tbl_binaries.c.version,
2140                                  maintainer_id = self.tbl_binaries.c.maintainer,
2141                                  maintainer = relation(Maintainer),
2142                                  source_id = self.tbl_binaries.c.source,
2143                                  source = relation(DBSource),
2144                                  arch_id = self.tbl_binaries.c.architecture,
2145                                  architecture = relation(Architecture),
2146                                  poolfile_id = self.tbl_binaries.c.file,
2147                                  poolfile = relation(PoolFile),
2148                                  binarytype = self.tbl_binaries.c.type,
2149                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
2150                                  fingerprint = relation(Fingerprint),
2151                                  install_date = self.tbl_binaries.c.install_date,
2152                                  binassociations = relation(BinAssociation,
2153                                                             primaryjoin=(self.tbl_binaries.c.id==self.tbl_bin_associations.c.bin))))
2154
2155         mapper(Component, self.tbl_component,
2156                properties = dict(component_id = self.tbl_component.c.id,
2157                                  component_name = self.tbl_component.c.name))
2158
2159         mapper(DBConfig, self.tbl_config,
2160                properties = dict(config_id = self.tbl_config.c.id))
2161
2162         mapper(ContentAssociation, self.tbl_content_associations,
2163                properties = dict(ca_id = self.tbl_content_associations.c.id,
2164                                  filename_id = self.tbl_content_associations.c.filename,
2165                                  filename    = relation(ContentFilename),
2166                                  filepath_id = self.tbl_content_associations.c.filepath,
2167                                  filepath    = relation(ContentFilepath),
2168                                  binary_id   = self.tbl_content_associations.c.binary_pkg,
2169                                  binary      = relation(DBBinary)))
2170
2171
2172         mapper(ContentFilename, self.tbl_content_file_names,
2173                properties = dict(cafilename_id = self.tbl_content_file_names.c.id,
2174                                  filename = self.tbl_content_file_names.c.file))
2175
2176         mapper(ContentFilepath, self.tbl_content_file_paths,
2177                properties = dict(cafilepath_id = self.tbl_content_file_paths.c.id,
2178                                  filepath = self.tbl_content_file_paths.c.path))
2179
2180         mapper(DSCFile, self.tbl_dsc_files,
2181                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
2182                                  source_id = self.tbl_dsc_files.c.source,
2183                                  source = relation(DBSource),
2184                                  poolfile_id = self.tbl_dsc_files.c.file,
2185                                  poolfile = relation(PoolFile)))
2186
2187         mapper(PoolFile, self.tbl_files,
2188                properties = dict(file_id = self.tbl_files.c.id,
2189                                  filesize = self.tbl_files.c.size,
2190                                  location_id = self.tbl_files.c.location,
2191                                  location = relation(Location)))
2192
2193         mapper(Fingerprint, self.tbl_fingerprint,
2194                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
2195                                  uid_id = self.tbl_fingerprint.c.uid,
2196                                  uid = relation(Uid),
2197                                  keyring_id = self.tbl_fingerprint.c.keyring,
2198                                  keyring = relation(Keyring)))
2199
2200         mapper(Keyring, self.tbl_keyrings,
2201                properties = dict(keyring_name = self.tbl_keyrings.c.name,
2202                                  keyring_id = self.tbl_keyrings.c.id))
2203
2204         mapper(Location, self.tbl_location,
2205                properties = dict(location_id = self.tbl_location.c.id,
2206                                  component_id = self.tbl_location.c.component,
2207                                  component = relation(Component),
2208                                  archive_id = self.tbl_location.c.archive,
2209                                  archive = relation(Archive),
2210                                  archive_type = self.tbl_location.c.type))
2211
2212         mapper(Maintainer, self.tbl_maintainer,
2213                properties = dict(maintainer_id = self.tbl_maintainer.c.id))
2214
2215         mapper(NewComment, self.tbl_new_comments,
2216                properties = dict(comment_id = self.tbl_new_comments.c.id))
2217
2218         mapper(Override, self.tbl_override,
2219                properties = dict(suite_id = self.tbl_override.c.suite,
2220                                  suite = relation(Suite),
2221                                  component_id = self.tbl_override.c.component,
2222                                  component = relation(Component),
2223                                  priority_id = self.tbl_override.c.priority,
2224                                  priority = relation(Priority),
2225                                  section_id = self.tbl_override.c.section,
2226                                  section = relation(Section),
2227                                  overridetype_id = self.tbl_override.c.type,
2228                                  overridetype = relation(OverrideType)))
2229
2230         mapper(OverrideType, self.tbl_override_type,
2231                properties = dict(overridetype = self.tbl_override_type.c.type,
2232                                  overridetype_id = self.tbl_override_type.c.id))
2233
2234         mapper(PendingContentAssociation, self.tbl_pending_content_associations,
2235                properties = dict(pca_id = self.tbl_pending_content_associations.c.id,
2236                                  filepath_id = self.tbl_pending_content_associations.c.filepath,
2237                                  filepath = relation(ContentFilepath),
2238                                  filename_id = self.tbl_pending_content_associations.c.filename,
2239                                  filename = relation(ContentFilename)))
2240
2241         mapper(Priority, self.tbl_priority,
2242                properties = dict(priority_id = self.tbl_priority.c.id))
2243
2244         mapper(Queue, self.tbl_queue,
2245                properties = dict(queue_id = self.tbl_queue.c.id))
2246
2247         mapper(QueueBuild, self.tbl_queue_build,
2248                properties = dict(suite_id = self.tbl_queue_build.c.suite,
2249                                  queue_id = self.tbl_queue_build.c.queue,
2250                                  queue = relation(Queue, backref='queuebuild')))
2251
2252         mapper(Section, self.tbl_section,
2253                properties = dict(section_id = self.tbl_section.c.id))
2254
2255         mapper(DBSource, self.tbl_source,
2256                properties = dict(source_id = self.tbl_source.c.id,
2257                                  version = self.tbl_source.c.version,
2258                                  maintainer_id = self.tbl_source.c.maintainer,
2259                                  maintainer = relation(Maintainer,
2260                                                        primaryjoin=(self.tbl_source.c.maintainer==self.tbl_maintainer.c.id)),
2261                                  poolfile_id = self.tbl_source.c.file,
2262                                  poolfile = relation(PoolFile),
2263                                  fingerprint_id = self.tbl_source.c.sig_fpr,
2264                                  fingerprint = relation(Fingerprint),
2265                                  changedby_id = self.tbl_source.c.changedby,
2266                                  changedby = relation(Maintainer,
2267                                                       primaryjoin=(self.tbl_source.c.changedby==self.tbl_maintainer.c.id)),
2268                                  srcfiles = relation(DSCFile,
2269                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
2270                                  srcassociations = relation(SrcAssociation,
2271                                                             primaryjoin=(self.tbl_source.c.id==self.tbl_src_associations.c.source))))
2272
2273         mapper(SrcAssociation, self.tbl_src_associations,
2274                properties = dict(sa_id = self.tbl_src_associations.c.id,
2275                                  suite_id = self.tbl_src_associations.c.suite,
2276                                  suite = relation(Suite),
2277                                  source_id = self.tbl_src_associations.c.source,
2278                                  source = relation(DBSource)))
2279
2280         mapper(SrcUploader, self.tbl_src_uploaders,
2281                properties = dict(uploader_id = self.tbl_src_uploaders.c.id,
2282                                  source_id = self.tbl_src_uploaders.c.source,
2283                                  source = relation(DBSource,
2284                                                    primaryjoin=(self.tbl_src_uploaders.c.source==self.tbl_source.c.id)),
2285                                  maintainer_id = self.tbl_src_uploaders.c.maintainer,
2286                                  maintainer = relation(Maintainer,
2287                                                        primaryjoin=(self.tbl_src_uploaders.c.maintainer==self.tbl_maintainer.c.id))))
2288
2289         mapper(Suite, self.tbl_suite,
2290                properties = dict(suite_id = self.tbl_suite.c.id))
2291
2292         mapper(SuiteArchitecture, self.tbl_suite_architectures,
2293                properties = dict(suite_id = self.tbl_suite_architectures.c.suite,
2294                                  suite = relation(Suite, backref='suitearchitectures'),
2295                                  arch_id = self.tbl_suite_architectures.c.architecture,
2296                                  architecture = relation(Architecture)))
2297
2298         mapper(Uid, self.tbl_uid,
2299                properties = dict(uid_id = self.tbl_uid.c.id,
2300                                  fingerprint = relation(Fingerprint)))
2301
2302     ## Connection functions
2303     def __createconn(self):
2304         from config import Config
2305         cnf = Config()
2306         if cnf["DB::Host"]:
2307             # TCP/IP
2308             connstr = "postgres://%s" % cnf["DB::Host"]
2309             if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
2310                 connstr += ":%s" % cnf["DB::Port"]
2311             connstr += "/%s" % cnf["DB::Name"]
2312         else:
2313             # Unix Socket
2314             connstr = "postgres:///%s" % cnf["DB::Name"]
2315             if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
2316                 connstr += "?port=%s" % cnf["DB::Port"]
2317
2318         self.db_pg   = create_engine(connstr, echo=self.debug)
2319         self.db_meta = MetaData()
2320         self.db_meta.bind = self.db_pg
2321         self.db_smaker = sessionmaker(bind=self.db_pg,
2322                                       autoflush=True,
2323                                       autocommit=False)
2324
2325         self.__setuptables()
2326         self.__setupmappers()
2327
2328     def session(self):
2329         return self.db_smaker()
2330
2331 __all__.append('DBConn')
2332
2333