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