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