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