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