]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
No need to assign to a local now that @session_wrapper does the cleanup
[dak.git] / daklib / dbconn.py
1 #!/usr/bin/python
2
3 """ DB access class
4
5 @contact: Debian FTPMaster <ftpmaster@debian.org>
6 @copyright: 2000, 2001, 2002, 2003, 2004, 2006  James Troup <james@nocrew.org>
7 @copyright: 2008-2009  Mark Hymers <mhy@debian.org>
8 @copyright: 2009  Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009  Mike O'Connor <stew@debian.org>
10 @license: GNU General Public License version 2 or later
11 """
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ################################################################################
28
29 # < mhy> I need a funny comment
30 # < sgran> two peanuts were walking down a dark street
31 # < sgran> one was a-salted
32 #  * mhy looks up the definition of "funny"
33
34 ################################################################################
35
36 import os
37 import psycopg2
38 import traceback
39
40 from inspect import getargspec
41
42 from sqlalchemy import create_engine, Table, MetaData, select
43 from sqlalchemy.orm import sessionmaker, mapper, relation
44
45 # Don't remove this, we re-export the exceptions to scripts which import us
46 from sqlalchemy.exc import *
47 from sqlalchemy.orm.exc import NoResultFound
48
49 # Only import Config until Queue stuff is changed to store its config
50 # in the database
51 from config import Config
52 from singleton import Singleton
53 from textutils import fix_maintainer
54
55 ################################################################################
56
57 __all__ = ['IntegrityError', 'SQLAlchemyError']
58
59 ################################################################################
60
61 def session_wrapper(fn):
62     def wrapped(*args, **kwargs):
63         private_transaction = False
64         session = kwargs.get('session')
65
66         # No session specified as last argument or in kwargs, create one.
67         if session is None and len(args) == len(getargspec(fn)[0]) - 1:
68             private_transaction = True
69             kwargs['session'] = DBConn().session()
70
71         try:
72             return fn(*args, **kwargs)
73         finally:
74             if private_transaction:
75                 # We created a session; close it.
76                 kwargs['session'].close()
77
78     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                     utils.copy(src, dest)
1430                 else:
1431                     # Create a symlink to it
1432                     os.symlink(src, dest)
1433
1434                 qb = QueueBuild()
1435                 qb.suite_id = s.suite_id
1436                 qb.queue_id = self.queue_id
1437                 qb.filename = dest
1438                 qb.in_queue = True
1439
1440                 session.add(qb)
1441
1442             # If the .orig.tar.gz is in the pool, create a symlink to
1443             # it (if one doesn't already exist)
1444             if changes.orig_tar_id:
1445                 # Determine the .orig.tar.gz file name
1446                 for dsc_file in changes.dsc_files.keys():
1447                     if dsc_file.endswith(".orig.tar.gz"):
1448                         filename = dsc_file
1449
1450                 dest = os.path.join(dest_dir, filename)
1451
1452                 # If it doesn't exist, create a symlink
1453                 if not os.path.exists(dest):
1454                     q = session.execute("SELECT l.path, f.filename FROM location l, files f WHERE f.id = :id and f.location = l.id",
1455                                         {'id': changes.orig_tar_id})
1456                     res = q.fetchone()
1457                     if not res:
1458                         return "[INTERNAL ERROR] Couldn't find id %s in files table." % (changes.orig_tar_id)
1459
1460                     src = os.path.join(res[0], res[1])
1461                     os.symlink(src, dest)
1462
1463                     # Add it to the list of packages for later processing by apt-ftparchive
1464                     qb = QueueBuild()
1465                     qb.suite_id = s.suite_id
1466                     qb.queue_id = self.queue_id
1467                     qb.filename = dest
1468                     qb.in_queue = True
1469                     session.add(qb)
1470
1471                 # If it does, update things to ensure it's not removed prematurely
1472                 else:
1473                     qb = get_queue_build(dest, s.suite_id, session)
1474                     if qb is None:
1475                         qb.in_queue = True
1476                         qb.last_used = None
1477                         session.add(qb)
1478
1479         if privatetrans:
1480             session.commit()
1481             session.close()
1482
1483         return None
1484
1485 __all__.append('Queue')
1486
1487 @session_wrapper
1488 def get_queue(queuename, session=None):
1489     """
1490     Returns Queue object for given C{queue name}.
1491
1492     @type queuename: string
1493     @param queuename: The name of the queue
1494
1495     @type session: Session
1496     @param session: Optional SQLA session object (a temporary one will be
1497     generated if not supplied)
1498
1499     @rtype: Queue
1500     @return: Queue object for the given queue
1501     """
1502
1503     q = session.query(Queue).filter_by(queue_name=queuename)
1504
1505     try:
1506         return q.one()
1507     except NoResultFound:
1508         return None
1509
1510 __all__.append('get_queue')
1511
1512 ################################################################################
1513
1514 class QueueBuild(object):
1515     def __init__(self, *args, **kwargs):
1516         pass
1517
1518     def __repr__(self):
1519         return '<QueueBuild %s (%s)>' % (self.filename, self.queue_id)
1520
1521 __all__.append('QueueBuild')
1522
1523 @session_wrapper
1524 def get_queue_build(filename, suite, session=None):
1525     """
1526     Returns QueueBuild object for given C{filename} and C{suite}.
1527
1528     @type filename: string
1529     @param filename: The name of the file
1530
1531     @type suiteid: int or str
1532     @param suiteid: Suite name or ID
1533
1534     @type session: Session
1535     @param session: Optional SQLA session object (a temporary one will be
1536     generated if not supplied)
1537
1538     @rtype: Queue
1539     @return: Queue object for the given queue
1540     """
1541
1542     if isinstance(suite, int):
1543         q = session.query(QueueBuild).filter_by(filename=filename).filter_by(suite_id=suite)
1544     else:
1545         q = session.query(QueueBuild).filter_by(filename=filename)
1546         q = q.join(Suite).filter_by(suite_name=suite)
1547
1548     try:
1549         return q.one()
1550     except NoResultFound:
1551         return None
1552
1553 __all__.append('get_queue_build')
1554
1555 ################################################################################
1556
1557 class Section(object):
1558     def __init__(self, *args, **kwargs):
1559         pass
1560
1561     def __eq__(self, val):
1562         if isinstance(val, str):
1563             return (self.section == val)
1564         # This signals to use the normal comparison operator
1565         return NotImplemented
1566
1567     def __ne__(self, val):
1568         if isinstance(val, str):
1569             return (self.section != val)
1570         # This signals to use the normal comparison operator
1571         return NotImplemented
1572
1573     def __repr__(self):
1574         return '<Section %s>' % self.section
1575
1576 __all__.append('Section')
1577
1578 @session_wrapper
1579 def get_section(section, session=None):
1580     """
1581     Returns Section object for given C{section name}.
1582
1583     @type section: string
1584     @param section: The name of the section
1585
1586     @type session: Session
1587     @param session: Optional SQLA session object (a temporary one will be
1588     generated if not supplied)
1589
1590     @rtype: Section
1591     @return: Section object for the given section name
1592     """
1593
1594     q = session.query(Section).filter_by(section=section)
1595
1596     try:
1597         return q.one()
1598     except NoResultFound:
1599         return None
1600
1601 __all__.append('get_section')
1602
1603 @session_wrapper
1604 def get_sections(session=None):
1605     """
1606     Returns dictionary of section names -> id mappings
1607
1608     @type session: Session
1609     @param session: Optional SQL session object (a temporary one will be
1610     generated if not supplied)
1611
1612     @rtype: dictionary
1613     @return: dictionary of section names -> id mappings
1614     """
1615
1616     ret = {}
1617     q = session.query(Section)
1618     for x in q.all():
1619         ret[x.section] = x.section_id
1620
1621     return ret
1622
1623 __all__.append('get_sections')
1624
1625 ################################################################################
1626
1627 class DBSource(object):
1628     def __init__(self, *args, **kwargs):
1629         pass
1630
1631     def __repr__(self):
1632         return '<DBSource %s (%s)>' % (self.source, self.version)
1633
1634 __all__.append('DBSource')
1635
1636 @session_wrapper
1637 def source_exists(source, source_version, suites = ["any"], session=None):
1638     """
1639     Ensure that source exists somewhere in the archive for the binary
1640     upload being processed.
1641       1. exact match     => 1.0-3
1642       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
1643
1644     @type package: string
1645     @param package: package source name
1646
1647     @type source_version: string
1648     @param source_version: expected source version
1649
1650     @type suites: list
1651     @param suites: list of suites to check in, default I{any}
1652
1653     @type session: Session
1654     @param session: Optional SQLA session object (a temporary one will be
1655     generated if not supplied)
1656
1657     @rtype: int
1658     @return: returns 1 if a source with expected version is found, otherwise 0
1659
1660     """
1661
1662     cnf = Config()
1663     ret = 1
1664
1665     for suite in suites:
1666         q = session.query(DBSource).filter_by(source=source)
1667         if suite != "any":
1668             # source must exist in suite X, or in some other suite that's
1669             # mapped to X, recursively... silent-maps are counted too,
1670             # unreleased-maps aren't.
1671             maps = cnf.ValueList("SuiteMappings")[:]
1672             maps.reverse()
1673             maps = [ m.split() for m in maps ]
1674             maps = [ (x[1], x[2]) for x in maps
1675                             if x[0] == "map" or x[0] == "silent-map" ]
1676             s = [suite]
1677             for x in maps:
1678                 if x[1] in s and x[0] not in s:
1679                     s.append(x[0])
1680
1681             q = q.join(SrcAssociation).join(Suite)
1682             q = q.filter(Suite.suite_name.in_(s))
1683
1684         # Reduce the query results to a list of version numbers
1685         ql = [ j.version for j in q.all() ]
1686
1687         # Try (1)
1688         if source_version in ql:
1689             continue
1690
1691         # Try (2)
1692         from daklib.regexes import re_bin_only_nmu
1693         orig_source_version = re_bin_only_nmu.sub('', source_version)
1694         if orig_source_version in ql:
1695             continue
1696
1697         # No source found so return not ok
1698         ret = 0
1699
1700     return ret
1701
1702 __all__.append('source_exists')
1703
1704 @session_wrapper
1705 def get_suites_source_in(source, session=None):
1706     """
1707     Returns list of Suite objects which given C{source} name is in
1708
1709     @type source: str
1710     @param source: DBSource package name to search for
1711
1712     @rtype: list
1713     @return: list of Suite objects for the given source
1714     """
1715
1716     return session.query(Suite).join(SrcAssociation).join(DBSource).filter_by(source=source).all()
1717
1718 __all__.append('get_suites_source_in')
1719
1720 @session_wrapper
1721 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
1722     """
1723     Returns list of DBSource objects for given C{source} name and other parameters
1724
1725     @type source: str
1726     @param source: DBSource package name to search for
1727
1728     @type source: str or None
1729     @param source: DBSource version name to search for or None if not applicable
1730
1731     @type dm_upload_allowed: bool
1732     @param dm_upload_allowed: If None, no effect.  If True or False, only
1733     return packages with that dm_upload_allowed setting
1734
1735     @type session: Session
1736     @param session: Optional SQL session object (a temporary one will be
1737     generated if not supplied)
1738
1739     @rtype: list
1740     @return: list of DBSource objects for the given name (may be empty)
1741     """
1742
1743     q = session.query(DBSource).filter_by(source=source)
1744
1745     if version is not None:
1746         q = q.filter_by(version=version)
1747
1748     if dm_upload_allowed is not None:
1749         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
1750
1751     return q.all()
1752
1753 __all__.append('get_sources_from_name')
1754
1755 @session_wrapper
1756 def get_source_in_suite(source, suite, session=None):
1757     """
1758     Returns list of DBSource objects for a combination of C{source} and C{suite}.
1759
1760       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
1761       - B{suite} - a suite name, eg. I{unstable}
1762
1763     @type source: string
1764     @param source: source package name
1765
1766     @type suite: string
1767     @param suite: the suite name
1768
1769     @rtype: string
1770     @return: the version for I{source} in I{suite}
1771
1772     """
1773
1774     q = session.query(SrcAssociation)
1775     q = q.join('source').filter_by(source=source)
1776     q = q.join('suite').filter_by(suite_name=suite)
1777
1778     try:
1779         return q.one().source
1780     except NoResultFound:
1781         return None
1782
1783 __all__.append('get_source_in_suite')
1784
1785 ################################################################################
1786
1787 class SrcAssociation(object):
1788     def __init__(self, *args, **kwargs):
1789         pass
1790
1791     def __repr__(self):
1792         return '<SrcAssociation %s (%s, %s)>' % (self.sa_id, self.source, self.suite)
1793
1794 __all__.append('SrcAssociation')
1795
1796 ################################################################################
1797
1798 class SrcUploader(object):
1799     def __init__(self, *args, **kwargs):
1800         pass
1801
1802     def __repr__(self):
1803         return '<SrcUploader %s>' % self.uploader_id
1804
1805 __all__.append('SrcUploader')
1806
1807 ################################################################################
1808
1809 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
1810                  ('SuiteID', 'suite_id'),
1811                  ('Version', 'version'),
1812                  ('Origin', 'origin'),
1813                  ('Label', 'label'),
1814                  ('Description', 'description'),
1815                  ('Untouchable', 'untouchable'),
1816                  ('Announce', 'announce'),
1817                  ('Codename', 'codename'),
1818                  ('OverrideCodename', 'overridecodename'),
1819                  ('ValidTime', 'validtime'),
1820                  ('Priority', 'priority'),
1821                  ('NotAutomatic', 'notautomatic'),
1822                  ('CopyChanges', 'copychanges'),
1823                  ('CopyDotDak', 'copydotdak'),
1824                  ('CommentsDir', 'commentsdir'),
1825                  ('OverrideSuite', 'overridesuite'),
1826                  ('ChangelogBase', 'changelogbase')]
1827
1828
1829 class Suite(object):
1830     def __init__(self, *args, **kwargs):
1831         pass
1832
1833     def __repr__(self):
1834         return '<Suite %s>' % self.suite_name
1835
1836     def __eq__(self, val):
1837         if isinstance(val, str):
1838             return (self.suite_name == val)
1839         # This signals to use the normal comparison operator
1840         return NotImplemented
1841
1842     def __ne__(self, val):
1843         if isinstance(val, str):
1844             return (self.suite_name != val)
1845         # This signals to use the normal comparison operator
1846         return NotImplemented
1847
1848     def details(self):
1849         ret = []
1850         for disp, field in SUITE_FIELDS:
1851             val = getattr(self, field, None)
1852             if val is not None:
1853                 ret.append("%s: %s" % (disp, val))
1854
1855         return "\n".join(ret)
1856
1857 __all__.append('Suite')
1858
1859 @session_wrapper
1860 def get_suite_architecture(suite, architecture, session=None):
1861     """
1862     Returns a SuiteArchitecture object given C{suite} and ${arch} or None if it
1863     doesn't exist
1864
1865     @type suite: str
1866     @param suite: Suite name to search for
1867
1868     @type architecture: str
1869     @param architecture: Architecture name to search for
1870
1871     @type session: Session
1872     @param session: Optional SQL session object (a temporary one will be
1873     generated if not supplied)
1874
1875     @rtype: SuiteArchitecture
1876     @return: the SuiteArchitecture object or None
1877     """
1878
1879     q = session.query(SuiteArchitecture)
1880     q = q.join(Architecture).filter_by(arch_string=architecture)
1881     q = q.join(Suite).filter_by(suite_name=suite)
1882
1883     try:
1884         return q.one()
1885     except NoResultFound:
1886         return None
1887
1888 __all__.append('get_suite_architecture')
1889
1890 @session_wrapper
1891 def get_suite(suite, session=None):
1892     """
1893     Returns Suite object for given C{suite name}.
1894
1895     @type suite: string
1896     @param suite: The name of the suite
1897
1898     @type session: Session
1899     @param session: Optional SQLA session object (a temporary one will be
1900     generated if not supplied)
1901
1902     @rtype: Suite
1903     @return: Suite object for the requested suite name (None if not presenT)
1904     """
1905
1906     q = session.query(Suite).filter_by(suite_name=suite)
1907
1908     try:
1909         return q.one()
1910     except NoResultFound:
1911         return None
1912
1913 __all__.append('get_suite')
1914
1915 ################################################################################
1916
1917 class SuiteArchitecture(object):
1918     def __init__(self, *args, **kwargs):
1919         pass
1920
1921     def __repr__(self):
1922         return '<SuiteArchitecture (%s, %s)>' % (self.suite_id, self.arch_id)
1923
1924 __all__.append('SuiteArchitecture')
1925
1926 @session_wrapper
1927 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
1928     """
1929     Returns list of Architecture objects for given C{suite} name
1930
1931     @type source: str
1932     @param source: Suite name to search for
1933
1934     @type skipsrc: boolean
1935     @param skipsrc: Whether to skip returning the 'source' architecture entry
1936     (Default False)
1937
1938     @type skipall: boolean
1939     @param skipall: Whether to skip returning the 'all' architecture entry
1940     (Default False)
1941
1942     @type session: Session
1943     @param session: Optional SQL session object (a temporary one will be
1944     generated if not supplied)
1945
1946     @rtype: list
1947     @return: list of Architecture objects for the given name (may be empty)
1948     """
1949
1950     q = session.query(Architecture)
1951     q = q.join(SuiteArchitecture)
1952     q = q.join(Suite).filter_by(suite_name=suite)
1953
1954     if skipsrc:
1955         q = q.filter(Architecture.arch_string != 'source')
1956
1957     if skipall:
1958         q = q.filter(Architecture.arch_string != 'all')
1959
1960     q = q.order_by('arch_string')
1961
1962     return q.all()
1963
1964 __all__.append('get_suite_architectures')
1965
1966 ################################################################################
1967
1968 class Uid(object):
1969     def __init__(self, *args, **kwargs):
1970         pass
1971
1972     def __eq__(self, val):
1973         if isinstance(val, str):
1974             return (self.uid == val)
1975         # This signals to use the normal comparison operator
1976         return NotImplemented
1977
1978     def __ne__(self, val):
1979         if isinstance(val, str):
1980             return (self.uid != val)
1981         # This signals to use the normal comparison operator
1982         return NotImplemented
1983
1984     def __repr__(self):
1985         return '<Uid %s (%s)>' % (self.uid, self.name)
1986
1987 __all__.append('Uid')
1988
1989 def add_database_user(uidname, session=None):
1990     """
1991     Adds a database user
1992
1993     @type uidname: string
1994     @param uidname: The uid of the user to add
1995
1996     @type session: SQLAlchemy
1997     @param session: Optional SQL session object (a temporary one will be
1998     generated if not supplied).  If not passed, a commit will be performed at
1999     the end of the function, otherwise the caller is responsible for commiting.
2000
2001     @rtype: Uid
2002     @return: the uid object for the given uidname
2003     """
2004
2005     privatetrans = False
2006     if session is None:
2007         session = DBConn().session()
2008         privatetrans = True
2009
2010     session.execute("CREATE USER :uid", {'uid': uidname})
2011
2012     if privatetrans:
2013         session.commit()
2014         session.close()
2015
2016 __all__.append('add_database_user')
2017
2018 def get_or_set_uid(uidname, session=None):
2019     """
2020     Returns uid object for given uidname.
2021
2022     If no matching uidname is found, a row is inserted.
2023
2024     @type uidname: string
2025     @param uidname: The uid to add
2026
2027     @type session: SQLAlchemy
2028     @param session: Optional SQL session object (a temporary one will be
2029     generated if not supplied).  If not passed, a commit will be performed at
2030     the end of the function, otherwise the caller is responsible for commiting.
2031
2032     @rtype: Uid
2033     @return: the uid object for the given uidname
2034     """
2035
2036     privatetrans = False
2037     if session is None:
2038         session = DBConn().session()
2039         privatetrans = True
2040
2041     q = session.query(Uid).filter_by(uid=uidname)
2042
2043     try:
2044         ret = q.one()
2045     except NoResultFound:
2046         uid = Uid()
2047         uid.uid = uidname
2048         session.add(uid)
2049         if privatetrans:
2050             session.commit()
2051         else:
2052             session.flush()
2053         ret = uid
2054
2055     if privatetrans:
2056         session.close()
2057
2058     return ret
2059
2060 __all__.append('get_or_set_uid')
2061
2062 @session_wrapper
2063 def get_uid_from_fingerprint(fpr, session=None):
2064     q = session.query(Uid)
2065     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
2066
2067     try:
2068         return q.one()
2069     except NoResultFound:
2070         return None
2071
2072 __all__.append('get_uid_from_fingerprint')
2073
2074 ################################################################################
2075
2076 class DBConn(Singleton):
2077     """
2078     database module init.
2079     """
2080     def __init__(self, *args, **kwargs):
2081         super(DBConn, self).__init__(*args, **kwargs)
2082
2083     def _startup(self, *args, **kwargs):
2084         self.debug = False
2085         if kwargs.has_key('debug'):
2086             self.debug = True
2087         self.__createconn()
2088
2089     def __setuptables(self):
2090         self.tbl_architecture = Table('architecture', self.db_meta, autoload=True)
2091         self.tbl_archive = Table('archive', self.db_meta, autoload=True)
2092         self.tbl_bin_associations = Table('bin_associations', self.db_meta, autoload=True)
2093         self.tbl_binaries = Table('binaries', self.db_meta, autoload=True)
2094         self.tbl_component = Table('component', self.db_meta, autoload=True)
2095         self.tbl_config = Table('config', self.db_meta, autoload=True)
2096         self.tbl_content_associations = Table('content_associations', self.db_meta, autoload=True)
2097         self.tbl_content_file_names = Table('content_file_names', self.db_meta, autoload=True)
2098         self.tbl_content_file_paths = Table('content_file_paths', self.db_meta, autoload=True)
2099         self.tbl_dsc_files = Table('dsc_files', self.db_meta, autoload=True)
2100         self.tbl_files = Table('files', self.db_meta, autoload=True)
2101         self.tbl_fingerprint = Table('fingerprint', self.db_meta, autoload=True)
2102         self.tbl_keyrings = Table('keyrings', self.db_meta, autoload=True)
2103         self.tbl_location = Table('location', self.db_meta, autoload=True)
2104         self.tbl_maintainer = Table('maintainer', self.db_meta, autoload=True)
2105         self.tbl_new_comments = Table('new_comments', self.db_meta, autoload=True)
2106         self.tbl_override = Table('override', self.db_meta, autoload=True)
2107         self.tbl_override_type = Table('override_type', self.db_meta, autoload=True)
2108         self.tbl_pending_content_associations = Table('pending_content_associations', self.db_meta, autoload=True)
2109         self.tbl_priority = Table('priority', self.db_meta, autoload=True)
2110         self.tbl_queue = Table('queue', self.db_meta, autoload=True)
2111         self.tbl_queue_build = Table('queue_build', self.db_meta, autoload=True)
2112         self.tbl_section = Table('section', self.db_meta, autoload=True)
2113         self.tbl_source = Table('source', self.db_meta, autoload=True)
2114         self.tbl_src_associations = Table('src_associations', self.db_meta, autoload=True)
2115         self.tbl_src_uploaders = Table('src_uploaders', self.db_meta, autoload=True)
2116         self.tbl_suite = Table('suite', self.db_meta, autoload=True)
2117         self.tbl_suite_architectures = Table('suite_architectures', self.db_meta, autoload=True)
2118         self.tbl_uid = Table('uid', self.db_meta, autoload=True)
2119
2120     def __setupmappers(self):
2121         mapper(Architecture, self.tbl_architecture,
2122                properties = dict(arch_id = self.tbl_architecture.c.id))
2123
2124         mapper(Archive, self.tbl_archive,
2125                properties = dict(archive_id = self.tbl_archive.c.id,
2126                                  archive_name = self.tbl_archive.c.name))
2127
2128         mapper(BinAssociation, self.tbl_bin_associations,
2129                properties = dict(ba_id = self.tbl_bin_associations.c.id,
2130                                  suite_id = self.tbl_bin_associations.c.suite,
2131                                  suite = relation(Suite),
2132                                  binary_id = self.tbl_bin_associations.c.bin,
2133                                  binary = relation(DBBinary)))
2134
2135         mapper(DBBinary, self.tbl_binaries,
2136                properties = dict(binary_id = self.tbl_binaries.c.id,
2137                                  package = self.tbl_binaries.c.package,
2138                                  version = self.tbl_binaries.c.version,
2139                                  maintainer_id = self.tbl_binaries.c.maintainer,
2140                                  maintainer = relation(Maintainer),
2141                                  source_id = self.tbl_binaries.c.source,
2142                                  source = relation(DBSource),
2143                                  arch_id = self.tbl_binaries.c.architecture,
2144                                  architecture = relation(Architecture),
2145                                  poolfile_id = self.tbl_binaries.c.file,
2146                                  poolfile = relation(PoolFile),
2147                                  binarytype = self.tbl_binaries.c.type,
2148                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
2149                                  fingerprint = relation(Fingerprint),
2150                                  install_date = self.tbl_binaries.c.install_date,
2151                                  binassociations = relation(BinAssociation,
2152                                                             primaryjoin=(self.tbl_binaries.c.id==self.tbl_bin_associations.c.bin))))
2153
2154         mapper(Component, self.tbl_component,
2155                properties = dict(component_id = self.tbl_component.c.id,
2156                                  component_name = self.tbl_component.c.name))
2157
2158         mapper(DBConfig, self.tbl_config,
2159                properties = dict(config_id = self.tbl_config.c.id))
2160
2161         mapper(ContentAssociation, self.tbl_content_associations,
2162                properties = dict(ca_id = self.tbl_content_associations.c.id,
2163                                  filename_id = self.tbl_content_associations.c.filename,
2164                                  filename    = relation(ContentFilename),
2165                                  filepath_id = self.tbl_content_associations.c.filepath,
2166                                  filepath    = relation(ContentFilepath),
2167                                  binary_id   = self.tbl_content_associations.c.binary_pkg,
2168                                  binary      = relation(DBBinary)))
2169
2170
2171         mapper(ContentFilename, self.tbl_content_file_names,
2172                properties = dict(cafilename_id = self.tbl_content_file_names.c.id,
2173                                  filename = self.tbl_content_file_names.c.file))
2174
2175         mapper(ContentFilepath, self.tbl_content_file_paths,
2176                properties = dict(cafilepath_id = self.tbl_content_file_paths.c.id,
2177                                  filepath = self.tbl_content_file_paths.c.path))
2178
2179         mapper(DSCFile, self.tbl_dsc_files,
2180                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
2181                                  source_id = self.tbl_dsc_files.c.source,
2182                                  source = relation(DBSource),
2183                                  poolfile_id = self.tbl_dsc_files.c.file,
2184                                  poolfile = relation(PoolFile)))
2185
2186         mapper(PoolFile, self.tbl_files,
2187                properties = dict(file_id = self.tbl_files.c.id,
2188                                  filesize = self.tbl_files.c.size,
2189                                  location_id = self.tbl_files.c.location,
2190                                  location = relation(Location)))
2191
2192         mapper(Fingerprint, self.tbl_fingerprint,
2193                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
2194                                  uid_id = self.tbl_fingerprint.c.uid,
2195                                  uid = relation(Uid),
2196                                  keyring_id = self.tbl_fingerprint.c.keyring,
2197                                  keyring = relation(Keyring)))
2198
2199         mapper(Keyring, self.tbl_keyrings,
2200                properties = dict(keyring_name = self.tbl_keyrings.c.name,
2201                                  keyring_id = self.tbl_keyrings.c.id))
2202
2203         mapper(Location, self.tbl_location,
2204                properties = dict(location_id = self.tbl_location.c.id,
2205                                  component_id = self.tbl_location.c.component,
2206                                  component = relation(Component),
2207                                  archive_id = self.tbl_location.c.archive,
2208                                  archive = relation(Archive),
2209                                  archive_type = self.tbl_location.c.type))
2210
2211         mapper(Maintainer, self.tbl_maintainer,
2212                properties = dict(maintainer_id = self.tbl_maintainer.c.id))
2213
2214         mapper(NewComment, self.tbl_new_comments,
2215                properties = dict(comment_id = self.tbl_new_comments.c.id))
2216
2217         mapper(Override, self.tbl_override,
2218                properties = dict(suite_id = self.tbl_override.c.suite,
2219                                  suite = relation(Suite),
2220                                  component_id = self.tbl_override.c.component,
2221                                  component = relation(Component),
2222                                  priority_id = self.tbl_override.c.priority,
2223                                  priority = relation(Priority),
2224                                  section_id = self.tbl_override.c.section,
2225                                  section = relation(Section),
2226                                  overridetype_id = self.tbl_override.c.type,
2227                                  overridetype = relation(OverrideType)))
2228
2229         mapper(OverrideType, self.tbl_override_type,
2230                properties = dict(overridetype = self.tbl_override_type.c.type,
2231                                  overridetype_id = self.tbl_override_type.c.id))
2232
2233         mapper(PendingContentAssociation, self.tbl_pending_content_associations,
2234                properties = dict(pca_id = self.tbl_pending_content_associations.c.id,
2235                                  filepath_id = self.tbl_pending_content_associations.c.filepath,
2236                                  filepath = relation(ContentFilepath),
2237                                  filename_id = self.tbl_pending_content_associations.c.filename,
2238                                  filename = relation(ContentFilename)))
2239
2240         mapper(Priority, self.tbl_priority,
2241                properties = dict(priority_id = self.tbl_priority.c.id))
2242
2243         mapper(Queue, self.tbl_queue,
2244                properties = dict(queue_id = self.tbl_queue.c.id))
2245
2246         mapper(QueueBuild, self.tbl_queue_build,
2247                properties = dict(suite_id = self.tbl_queue_build.c.suite,
2248                                  queue_id = self.tbl_queue_build.c.queue,
2249                                  queue = relation(Queue, backref='queuebuild')))
2250
2251         mapper(Section, self.tbl_section,
2252                properties = dict(section_id = self.tbl_section.c.id))
2253
2254         mapper(DBSource, self.tbl_source,
2255                properties = dict(source_id = self.tbl_source.c.id,
2256                                  version = self.tbl_source.c.version,
2257                                  maintainer_id = self.tbl_source.c.maintainer,
2258                                  maintainer = relation(Maintainer,
2259                                                        primaryjoin=(self.tbl_source.c.maintainer==self.tbl_maintainer.c.id)),
2260                                  poolfile_id = self.tbl_source.c.file,
2261                                  poolfile = relation(PoolFile),
2262                                  fingerprint_id = self.tbl_source.c.sig_fpr,
2263                                  fingerprint = relation(Fingerprint),
2264                                  changedby_id = self.tbl_source.c.changedby,
2265                                  changedby = relation(Maintainer,
2266                                                       primaryjoin=(self.tbl_source.c.changedby==self.tbl_maintainer.c.id)),
2267                                  srcfiles = relation(DSCFile,
2268                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
2269                                  srcassociations = relation(SrcAssociation,
2270                                                             primaryjoin=(self.tbl_source.c.id==self.tbl_src_associations.c.source))))
2271
2272         mapper(SrcAssociation, self.tbl_src_associations,
2273                properties = dict(sa_id = self.tbl_src_associations.c.id,
2274                                  suite_id = self.tbl_src_associations.c.suite,
2275                                  suite = relation(Suite),
2276                                  source_id = self.tbl_src_associations.c.source,
2277                                  source = relation(DBSource)))
2278
2279         mapper(SrcUploader, self.tbl_src_uploaders,
2280                properties = dict(uploader_id = self.tbl_src_uploaders.c.id,
2281                                  source_id = self.tbl_src_uploaders.c.source,
2282                                  source = relation(DBSource,
2283                                                    primaryjoin=(self.tbl_src_uploaders.c.source==self.tbl_source.c.id)),
2284                                  maintainer_id = self.tbl_src_uploaders.c.maintainer,
2285                                  maintainer = relation(Maintainer,
2286                                                        primaryjoin=(self.tbl_src_uploaders.c.maintainer==self.tbl_maintainer.c.id))))
2287
2288         mapper(Suite, self.tbl_suite,
2289                properties = dict(suite_id = self.tbl_suite.c.id))
2290
2291         mapper(SuiteArchitecture, self.tbl_suite_architectures,
2292                properties = dict(suite_id = self.tbl_suite_architectures.c.suite,
2293                                  suite = relation(Suite, backref='suitearchitectures'),
2294                                  arch_id = self.tbl_suite_architectures.c.architecture,
2295                                  architecture = relation(Architecture)))
2296
2297         mapper(Uid, self.tbl_uid,
2298                properties = dict(uid_id = self.tbl_uid.c.id,
2299                                  fingerprint = relation(Fingerprint)))
2300
2301     ## Connection functions
2302     def __createconn(self):
2303         from config import Config
2304         cnf = Config()
2305         if cnf["DB::Host"]:
2306             # TCP/IP
2307             connstr = "postgres://%s" % cnf["DB::Host"]
2308             if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
2309                 connstr += ":%s" % cnf["DB::Port"]
2310             connstr += "/%s" % cnf["DB::Name"]
2311         else:
2312             # Unix Socket
2313             connstr = "postgres:///%s" % cnf["DB::Name"]
2314             if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
2315                 connstr += "?port=%s" % cnf["DB::Port"]
2316
2317         self.db_pg   = create_engine(connstr, echo=self.debug)
2318         self.db_meta = MetaData()
2319         self.db_meta.bind = self.db_pg
2320         self.db_smaker = sessionmaker(bind=self.db_pg,
2321                                       autoflush=True,
2322                                       autocommit=False)
2323
2324         self.__setuptables()
2325         self.__setupmappers()
2326
2327     def session(self):
2328         return self.db_smaker()
2329
2330 __all__.append('DBConn')
2331
2332