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