Browse Source

Merge branch 'refs/heads/develop'

pull/381/merge
Ruud 13 years ago
parent
commit
d89130dc30
  1. 15
      couchpotato/core/helpers/variable.py
  2. 8
      couchpotato/core/notifications/core/main.py
  3. 2
      couchpotato/core/notifications/history/main.py
  4. 4
      couchpotato/core/plugins/file/main.py
  5. 6
      couchpotato/core/plugins/library/main.py
  6. 18
      couchpotato/core/plugins/movie/main.py
  7. 12
      couchpotato/core/plugins/profile/main.py
  8. 8
      couchpotato/core/plugins/quality/main.py
  9. 10
      couchpotato/core/plugins/release/main.py
  10. 13
      couchpotato/core/plugins/renamer/main.py
  11. 4
      couchpotato/core/plugins/scanner/main.py
  12. 3
      couchpotato/core/plugins/score/main.py
  13. 21
      couchpotato/core/plugins/searcher/main.py
  14. 8
      couchpotato/core/plugins/status/main.py
  15. 2
      couchpotato/core/plugins/subtitle/main.py
  16. 3
      couchpotato/core/plugins/userscript/template.js
  17. 3
      couchpotato/core/providers/metadata/xbmc/main.py
  18. 2
      couchpotato/core/providers/movie/_modifier/main.py
  19. 2
      couchpotato/core/providers/movie/couchpotatoapi/main.py
  20. 4
      couchpotato/core/providers/movie/imdbapi/main.py
  21. 8
      couchpotato/core/providers/movie/themoviedb/main.py
  22. 3
      couchpotato/core/providers/nzb/moovee/main.py
  23. 4
      couchpotato/core/providers/nzb/mysterbin/main.py
  24. 4
      couchpotato/core/providers/nzb/nzbclub/main.py
  25. 4
      couchpotato/core/providers/nzb/nzbindex/main.py
  26. 4
      couchpotato/core/providers/nzb/x264/main.py
  27. 4
      couchpotato/core/providers/torrent/kickasstorrents/main.py
  28. 6
      couchpotato/core/providers/trailer/hdtrailers/main.py
  29. 4
      couchpotato/core/settings/__init__.py
  30. 3
      couchpotato/environment.py
  31. 5
      couchpotato/runner.py
  32. 7
      libs/elixir/__init__.py
  33. 4
      libs/elixir/collection.py
  34. 197
      libs/elixir/entity.py
  35. 9
      libs/elixir/events.py
  36. 4
      libs/elixir/ext/associable.py
  37. 251
      libs/elixir/ext/list.py
  38. 11
      libs/elixir/options.py
  39. 73
      libs/elixir/py23compat.py
  40. 50
      libs/elixir/relationships.py

15
couchpotato/core/helpers/variable.py

@ -1,8 +1,11 @@
from couchpotato.core.logger import CPLog
import hashlib import hashlib
import os.path import os.path
import platform import platform
import re import re
log = CPLog(__name__)
def getDataDir(): def getDataDir():
# Windows # Windows
@ -102,3 +105,15 @@ def natsortKey(s):
def natcmp(a, b): def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b)) return cmp(natsortKey(a), natsortKey(b))
def getTitle(library_dict):
try:
try:
return library_dict['titles'][0]['title']
except:
log.error('Could not get title for %s' % library_dict['identifier'])
return None
except:
log.error('Could not get title for library item: %s' % library_dict)
return None

8
couchpotato/core/notifications/core/main.py

@ -65,7 +65,7 @@ class CoreNotifier(Notification):
q.update({Notif.read: True}) q.update({Notif.read: True})
db.commit() db.commit()
db.close() #db.close()
return jsonified({ return jsonified({
'success': True 'success': True
@ -91,7 +91,7 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
notifications.append(ndict) notifications.append(ndict)
db.close() #db.close()
return jsonified({ return jsonified({
'success': True, 'success': True,
'empty': len(notifications) == 0, 'empty': len(notifications) == 0,
@ -116,7 +116,7 @@ class CoreNotifier(Notification):
ndict['time'] = time.time() ndict['time'] = time.time()
self.messages.append(ndict) self.messages.append(ndict)
db.close() #db.close()
return True return True
def frontend(self, type = 'notification', data = {}): def frontend(self, type = 'notification', data = {}):
@ -146,7 +146,7 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
messages.append(ndict) messages.append(ndict)
db.close() #db.close()
self.messages = [] self.messages = []
return jsonified({ return jsonified({

2
couchpotato/core/notifications/history/main.py

@ -22,6 +22,6 @@ class History(Notification):
) )
db.add(history) db.add(history)
db.commit() db.commit()
db.close() #db.close()
return True return True

4
couchpotato/core/plugins/file/main.py

@ -87,7 +87,7 @@ class FileManager(Plugin):
db.commit() db.commit()
type_dict = ft.to_dict() type_dict = ft.to_dict()
db.close() #db.close()
return type_dict return type_dict
def getTypes(self): def getTypes(self):
@ -100,5 +100,5 @@ class FileManager(Plugin):
for type_object in results: for type_object in results:
types.append(type_object.to_dict()) types.append(type_object.to_dict())
db.close() #db.close()
return types return types

6
couchpotato/core/plugins/library/main.py

@ -53,7 +53,7 @@ class LibraryPlugin(Plugin):
library_dict = l.to_dict(self.default_dict) library_dict = l.to_dict(self.default_dict)
db.close() #db.close()
return library_dict return library_dict
def update(self, identifier, default_title = '', force = False): def update(self, identifier, default_title = '', force = False):
@ -130,7 +130,7 @@ class LibraryPlugin(Plugin):
fireEvent('library.update_finish', data = library_dict) fireEvent('library.update_finish', data = library_dict)
db.close() #db.close()
return library_dict return library_dict
def updateReleaseDate(self, identifier): def updateReleaseDate(self, identifier):
@ -144,7 +144,7 @@ class LibraryPlugin(Plugin):
db.commit() db.commit()
dates = library.info.get('release_date', {}) dates = library.info.get('release_date', {})
db.close() #db.close()
return dates return dates

18
couchpotato/core/plugins/movie/main.py

@ -113,7 +113,7 @@ class MoviePlugin(Plugin):
if m: if m:
results = m.to_dict(self.default_dict) results = m.to_dict(self.default_dict)
db.close() #db.close()
return results return results
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None): def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
@ -177,7 +177,7 @@ class MoviePlugin(Plugin):
}) })
movies.append(temp) movies.append(temp)
db.close() #db.close()
return movies return movies
def availableChars(self, status = ['active']): def availableChars(self, status = ['active']):
@ -203,7 +203,7 @@ class MoviePlugin(Plugin):
if char not in chars: if char not in chars:
chars += char chars += char
db.close() #db.close()
return chars return chars
def listView(self): def listView(self):
@ -250,7 +250,7 @@ class MoviePlugin(Plugin):
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True) fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
fireEventAsync('searcher.single', movie.to_dict(self.default_dict)) fireEventAsync('searcher.single', movie.to_dict(self.default_dict))
db.close() #db.close()
return jsonified({ return jsonified({
'success': True, 'success': True,
}) })
@ -324,7 +324,7 @@ class MoviePlugin(Plugin):
if (force_readd or do_search) and search_after: if (force_readd or do_search) and search_after:
fireEventAsync('searcher.single', movie_dict) fireEventAsync('searcher.single', movie_dict)
db.close() #db.close()
return movie_dict return movie_dict
@ -371,7 +371,7 @@ class MoviePlugin(Plugin):
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.default_dict)
fireEventAsync('searcher.single', movie_dict) fireEventAsync('searcher.single', movie_dict)
db.close() #db.close()
return jsonified({ return jsonified({
'success': True, 'success': True,
}) })
@ -426,7 +426,7 @@ class MoviePlugin(Plugin):
else: else:
fireEvent('movie.restatus', movie.id, single = True) fireEvent('movie.restatus', movie.id, single = True)
db.close() #db.close()
return True return True
def restatus(self, movie_id): def restatus(self, movie_id):
@ -437,7 +437,7 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
if not m: if not m or len(m.library.titles) == 0:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.') log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False return False
@ -455,6 +455,6 @@ class MoviePlugin(Plugin):
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit() db.commit()
db.close() #db.close()
return True return True

12
couchpotato/core/plugins/profile/main.py

@ -47,7 +47,7 @@ class ProfilePlugin(Plugin):
for profile in profiles: for profile in profiles:
temp.append(profile.to_dict(self.to_dict)) temp.append(profile.to_dict(self.to_dict))
db.close() #db.close()
return temp return temp
def save(self): def save(self):
@ -84,7 +84,7 @@ class ProfilePlugin(Plugin):
profile_dict = p.to_dict(self.to_dict) profile_dict = p.to_dict(self.to_dict)
db.close() #db.close()
return jsonified({ return jsonified({
'success': True, 'success': True,
'profile': profile_dict 'profile': profile_dict
@ -95,7 +95,7 @@ class ProfilePlugin(Plugin):
db = get_session() db = get_session()
default = db.query(Profile).first() default = db.query(Profile).first()
default_dict = default.to_dict(self.to_dict) default_dict = default.to_dict(self.to_dict)
db.close() #db.close()
return default_dict return default_dict
@ -113,7 +113,7 @@ class ProfilePlugin(Plugin):
order += 1 order += 1
db.commit() db.commit()
db.close() #db.close()
return jsonified({ return jsonified({
'success': True 'success': True
@ -138,7 +138,7 @@ class ProfilePlugin(Plugin):
message = 'Failed deleting Profile: %s' % e message = 'Failed deleting Profile: %s' % e
log.error(message) log.error(message)
db.close() #db.close()
return jsonified({ return jsonified({
'success': success, 'success': success,
@ -187,5 +187,5 @@ class ProfilePlugin(Plugin):
order += 1 order += 1
db.close() #db.close()
return True return True

8
couchpotato/core/plugins/quality/main.py

@ -68,7 +68,7 @@ class QualityPlugin(Plugin):
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict()) q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
temp.append(q) temp.append(q)
db.close() #db.close()
return temp return temp
def single(self, identifier = ''): def single(self, identifier = ''):
@ -80,7 +80,7 @@ class QualityPlugin(Plugin):
if quality: if quality:
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict()) quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
db.close() #db.close()
return quality_dict return quality_dict
def getQuality(self, identifier): def getQuality(self, identifier):
@ -100,7 +100,7 @@ class QualityPlugin(Plugin):
setattr(quality, params.get('value_type'), params.get('value')) setattr(quality, params.get('value_type'), params.get('value'))
db.commit() db.commit()
db.close() #db.close()
return jsonified({ return jsonified({
'success': True 'success': True
}) })
@ -152,7 +152,7 @@ class QualityPlugin(Plugin):
order += 1 order += 1
db.commit() db.commit()
db.close() #db.close()
return True return True
def guess(self, files, extra = {}): def guess(self, files, extra = {}):

10
couchpotato/core/plugins/release/main.py

@ -83,7 +83,7 @@ class Release(Plugin):
fireEvent('movie.restatus', movie.id) fireEvent('movie.restatus', movie.id)
db.close() #db.close()
return True return True
@ -109,7 +109,7 @@ class Release(Plugin):
rel.delete() rel.delete()
db.commit() db.commit()
db.close() #db.close()
return jsonified({ return jsonified({
'success': True 'success': True
}) })
@ -126,7 +126,7 @@ class Release(Plugin):
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
db.commit() db.commit()
db.close() #db.close()
return jsonified({ return jsonified({
'success': True 'success': True
}) })
@ -153,14 +153,14 @@ class Release(Plugin):
'files': {} 'files': {}
}), manual = True) }), manual = True)
db.close() #db.close()
return jsonified({ return jsonified({
'success': True 'success': True
}) })
else: else:
log.error('Couldn\'t find release with id: %s' % id) log.error('Couldn\'t find release with id: %s' % id)
db.close() #db.close()
return jsonified({ return jsonified({
'success': False 'success': False
}) })

13
couchpotato/core/plugins/renamer/main.py

@ -3,7 +3,7 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile from couchpotato.core.settings.model import Library, File, Profile
@ -82,8 +82,10 @@ class Renamer(Plugin):
remove_files = [] remove_files = []
remove_releases = [] remove_releases = []
movie_title = getTitle(group['library'])
# Add _UNKNOWN_ if no library item is connected # Add _UNKNOWN_ if no library item is connected
if not group['library']: if not group['library'] or not movie_title:
if group['dirname']: if group['dirname']:
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname']) rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname'])
else: # Add it to filename else: # Add it to filename
@ -100,12 +102,13 @@ class Renamer(Plugin):
continue continue
library = group['library'] library = group['library']
movie_title = getTitle(library)
# Find subtitle for renaming # Find subtitle for renaming
fireEvent('renamer.before', group) fireEvent('renamer.before', group)
# Remove weird chars from moviename # Remove weird chars from moviename
movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', group['library']['titles'][0]['title']) movie_name = re.sub(r"[\x00\/\\:\*\?\"<>\|]", '', movie_title)
# Put 'The' at the end # Put 'The' at the end
name_the = movie_name name_the = movie_name
@ -369,14 +372,14 @@ class Renamer(Plugin):
fireEventAsync('renamer.after', group) fireEventAsync('renamer.after', group)
# Notify on download # Notify on download
download_message = 'Downloaded %s (%s)' % (group['library']['titles'][0]['title'], replacements['quality']) download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
fireEventAsync('movie.downloaded', message = download_message, data = group) fireEventAsync('movie.downloaded', message = download_message, data = group)
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
break break
db.close() #db.close()
self.renaming_started = False self.renaming_started = False
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''): def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''):

4
couchpotato/core/plugins/scanner/main.py

@ -8,7 +8,7 @@ from couchpotato.core.settings.model import File
from couchpotato.environment import Env from couchpotato.environment import Env
from enzyme.exceptions import NoParserError, ParseError from enzyme.exceptions import NoParserError, ParseError
from guessit import guess_movie_info from guessit import guess_movie_info
from subliminal.videos import scan, Video from subliminal.videos import Video
import enzyme import enzyme
import os import os
import re import re
@ -455,7 +455,7 @@ class Scanner(Plugin):
break break
except: except:
pass pass
db.close() #db.close()
# Search based on OpenSubtitleHash # Search based on OpenSubtitleHash
if not imdb_id and not group['is_dvd']: if not imdb_id and not group['is_dvd']:

3
couchpotato/core/plugins/score/main.py

@ -1,5 +1,6 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \ from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
@ -35,6 +36,6 @@ class Score(Plugin):
score += providerScore(nzb['provider']) score += providerScore(nzb['provider'])
# Duplicates in name # Duplicates in name
score += duplicateScore(nzb['name'], movie['library']['titles'][0]['title']) score += duplicateScore(nzb['name'], getTitle(movie['library']))
return score return score

21
couchpotato/core/plugins/searcher/main.py

@ -1,7 +1,7 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.variable import md5, getImdb from couchpotato.core.helpers.variable import md5, getImdb, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
@ -61,7 +61,7 @@ class Searcher(Plugin):
if self.shuttingDown(): if self.shuttingDown():
break break
db.close() #db.close()
self.in_progress = False self.in_progress = False
def single(self, movie): def single(self, movie):
@ -78,7 +78,10 @@ class Searcher(Plugin):
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
default_title = movie['library']['titles'][0]['title'] default_title = getTitle(movie['library'])
if not default_title:
return
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases): if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title)) log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title))
@ -101,6 +104,10 @@ class Searcher(Plugin):
if len(sorted_results) == 0: if len(sorted_results) == 0:
log.debug('Nothing found for %s in %s' % (default_title, quality_type['quality']['label'])) log.debug('Nothing found for %s in %s' % (default_title, quality_type['quality']['label']))
# Check if movie isn't deleted while searching
if not db.query(Movie).filter_by(id = movie.get('id')).first():
return
# Add them to this movie releases list # Add them to this movie releases list
for nzb in sorted_results: for nzb in sorted_results:
@ -148,7 +155,7 @@ class Searcher(Plugin):
if self.shuttingDown(): if self.shuttingDown():
break break
db.close() #db.close()
return False return False
def download(self, data, movie, manual = False): def download(self, data, movie, manual = False):
@ -164,7 +171,7 @@ class Searcher(Plugin):
rls.status_id = snatched_status.get('id') rls.status_id = snatched_status.get('id')
db.commit() db.commit()
log_movie = '%s (%s) in %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], rls.quality.label) log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
log.info(snatch_message) log.info(snatch_message)
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
@ -191,7 +198,7 @@ class Searcher(Plugin):
except Exception, e: except Exception, e:
log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc())) log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc()))
db.close() #db.close()
return True return True
log.info('Tried to download, but none of the downloaders are enabled') log.info('Tried to download, but none of the downloaders are enabled')
@ -270,7 +277,7 @@ class Searcher(Plugin):
if self.checkNFO(nzb['name'], movie['library']['identifier']): if self.checkNFO(nzb['name'], movie['library']['identifier']):
return True return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie['library']['titles'][0]['title'], movie['library']['year'])) log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], getTitle(movie['library']), movie['library']['year']))
return False return False
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False): def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False):

8
couchpotato/core/plugins/status/main.py

@ -49,7 +49,7 @@ class StatusPlugin(Plugin):
db = get_session() db = get_session()
status = db.query(Status).filter_by(id = id).first() status = db.query(Status).filter_by(id = id).first()
status_dict = status.to_dict() status_dict = status.to_dict()
db.close() #db.close()
return status_dict return status_dict
@ -64,7 +64,7 @@ class StatusPlugin(Plugin):
s = status.to_dict() s = status.to_dict()
temp.append(s) temp.append(s)
db.close() #db.close()
return temp return temp
def add(self, identifier): def add(self, identifier):
@ -82,7 +82,7 @@ class StatusPlugin(Plugin):
status_dict = s.to_dict() status_dict = s.to_dict()
db.close() #db.close()
return status_dict return status_dict
def fill(self): def fill(self):
@ -102,5 +102,5 @@ class StatusPlugin(Plugin):
s.label = toUnicode(label) s.label = toUnicode(label)
db.commit() db.commit()
db.close() #db.close()

2
couchpotato/core/plugins/subtitle/main.py

@ -38,7 +38,7 @@ class Subtitle(Plugin):
# get subtitles for those files # get subtitles for those files
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services) subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
db.close() #db.close()
def searchSingle(self, group): def searchSingle(self, group):

3
couchpotato/core/plugins/userscript/template.js

@ -12,6 +12,9 @@
// ==/UserScript== // ==/UserScript==
if (window.top != window.self) // Only run on top window
return;
var version = {{version}}, var version = {{version}},
host = '{{host}}', host = '{{host}}',
api = '{{api}}'; api = '{{api}}';

3
couchpotato/core/providers/metadata/xbmc/main.py

@ -1,4 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.metadata.base import MetaDataBase from couchpotato.core.providers.metadata.base import MetaDataBase
from xml.etree.ElementTree import Element, SubElement, tostring from xml.etree.ElementTree import Element, SubElement, tostring
@ -32,7 +33,7 @@ class XBMC(MetaDataBase):
# Title # Title
try: try:
el = SubElement(nfoxml, 'title') el = SubElement(nfoxml, 'title')
el.text = toUnicode(data['library']['titles'][0]['title']) el.text = toUnicode(getTitle(data['library']))
except: except:
pass pass

2
couchpotato/core/providers/movie/_modifier/main.py

@ -63,7 +63,7 @@ class MovieResultModifier(Plugin):
except: except:
log.error('Tried getting more info on searched movies: %s' % traceback.format_exc()) log.error('Tried getting more info on searched movies: %s' % traceback.format_exc())
db.close() #db.close()
return temp return temp
def checkLibrary(self, result): def checkLibrary(self, result):

2
couchpotato/core/providers/movie/couchpotatoapi/main.py

@ -59,7 +59,7 @@ class CouchPotatoApi(MovieProvider):
db = get_session() db = get_session()
active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all() active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all()
movies = [x.library.identifier for x in active_movies] movies = [x.library.identifier for x in active_movies]
db.close() #db.close()
suggestions = self.suggest(movies, ignore) suggestions = self.suggest(movies, ignore)

4
couchpotato/core/providers/movie/imdbapi/main.py

@ -64,8 +64,12 @@ class IMDBAPI(MovieProvider):
movie_data = {} movie_data = {}
try: try:
try:
if isinstance(movie, (str, unicode)): if isinstance(movie, (str, unicode)):
movie = json.loads(movie) movie = json.loads(movie)
except ValueError:
log.info('No proper json to decode')
return movie_data
if movie.get('Response') == 'Parse Error': if movie.get('Response') == 'Parse Error':
return movie_data return movie_data

8
couchpotato/core/providers/movie/themoviedb/main.py

@ -69,13 +69,7 @@ class TheMovieDb(MovieProvider):
try: try:
nr = 0 nr = 0
# Sort on returned score first when year is in q for movie in raw:
if re.search('\s\d{4}', q):
movies = sorted(raw, key = lambda k: k['score'], reverse = True)
else:
movies = raw
for movie in movies:
results.append(self.parseMovie(movie)) results.append(self.parseMovie(movie))
nr += 1 nr += 1

3
couchpotato/core/providers/nzb/moovee/main.py

@ -1,5 +1,6 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse from dateutil.parser import parse
@ -26,7 +27,7 @@ class Moovee(NZBProvider):
if self.isDisabled() or not self.isAvailable(self.urls['search']) or quality.get('hd', False): if self.isDisabled() or not self.isAvailable(self.urls['search']) or quality.get('hd', False):
return results return results
q = '%s %s' % (movie['library']['titles'][0]['title'], quality.get('identifier')) q = '%s %s' % (getTitle(movie['library']), quality.get('identifier'))
url = self.urls['search'] % tryUrlencode(q) url = self.urls['search'] % tryUrlencode(q)
cache_key = 'moovee.%s' % q cache_key = 'moovee.%s' % q

4
couchpotato/core/providers/nzb/mysterbin/main.py

@ -1,7 +1,7 @@
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
@ -25,7 +25,7 @@ class Mysterbin(NZBProvider):
if self.isDisabled() or not self.isAvailable(self.urls['search']): if self.isDisabled() or not self.isAvailable(self.urls['search']):
return results return results
q = '"%s" %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier')) q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
for ignored in Env.setting('ignored_words', 'searcher').split(','): for ignored in Env.setting('ignored_words', 'searcher').split(','):
q = '%s -%s' % (q, ignored.strip()) q = '%s -%s' % (q, ignored.strip())

4
couchpotato/core/providers/nzb/nzbclub/main.py

@ -2,7 +2,7 @@ from BeautifulSoup import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
@ -27,7 +27,7 @@ class NZBClub(NZBProvider, RSS):
if self.isDisabled() or not self.isAvailable(self.urls['search']): if self.isDisabled() or not self.isAvailable(self.urls['search']):
return results return results
q = '"%s" %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier')) q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
for ignored in Env.setting('ignored_words', 'searcher').split(','): for ignored in Env.setting('ignored_words', 'searcher').split(','):
q = '%s -%s' % (q, ignored.strip()) q = '%s -%s' % (q, ignored.strip())

4
couchpotato/core/providers/nzb/nzbindex/main.py

@ -2,7 +2,7 @@ from BeautifulSoup import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
@ -29,7 +29,7 @@ class NzbIndex(NZBProvider, RSS):
if self.isDisabled() or not self.isAvailable(self.urls['api']): if self.isDisabled() or not self.isAvailable(self.urls['api']):
return results return results
q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier')) q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
arguments = tryUrlencode({ arguments = tryUrlencode({
'q': q, 'q': q,
'age': Env.setting('retention', 'nzb'), 'age': Env.setting('retention', 'nzb'),

4
couchpotato/core/providers/nzb/x264/main.py

@ -1,6 +1,6 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
import re import re
@ -25,7 +25,7 @@ class X264(NZBProvider):
if self.isDisabled() or not self.isAvailable(self.urls['search'].split('requests')[0]) or not quality.get('hd', False): if self.isDisabled() or not self.isAvailable(self.urls['search'].split('requests')[0]) or not quality.get('hd', False):
return results return results
q = '%s %s %s' % (movie['library']['titles'][0]['title'], movie['library']['year'], quality.get('identifier')) q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
url = self.urls['search'] % tryUrlencode(q) url = self.urls['search'] % tryUrlencode(q)
cache_key = 'x264.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) cache_key = 'x264.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))

4
couchpotato/core/providers/torrent/kickasstorrents/main.py

@ -1,6 +1,6 @@
from BeautifulSoup import BeautifulSoup from BeautifulSoup import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
import StringIO import StringIO
@ -38,7 +38,7 @@ class KickAssTorrents(TorrentProvider):
return results return results
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, self.urls['search'] % (movie['library']['titles'][0]['title'], movie['library']['identifier'].replace('tt', ''))) data = self.getCache(cache_key, self.urls['search'] % (getTitle(movie['library']), movie['library']['identifier'].replace('tt', '')))
if data: if data:
cat_ids = self.getCatId(quality['identifier']) cat_ids = self.getCatId(quality['identifier'])

6
couchpotato/core/providers/trailer/hdtrailers/main.py

@ -1,6 +1,6 @@
from BeautifulSoup import SoupStrainer, BeautifulSoup from BeautifulSoup import SoupStrainer, BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.helpers.variable import mergeDicts, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.trailer.base import TrailerProvider from couchpotato.core.providers.trailer.base import TrailerProvider
from string import letters, digits from string import letters, digits
@ -19,7 +19,7 @@ class HDTrailers(TrailerProvider):
def search(self, group): def search(self, group):
movie_name = group['library']['titles'][0]['title'] movie_name = getTitle(group['library'])
url = self.urls['api'] % self.movieUrlName(movie_name) url = self.urls['api'] % self.movieUrlName(movie_name)
data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url) data = self.getCache('hdtrailers.%s' % group['library']['identifier'], url)
@ -44,7 +44,7 @@ class HDTrailers(TrailerProvider):
def findViaAlternative(self, group): def findViaAlternative(self, group):
results = {'480p':[], '720p':[], '1080p':[]} results = {'480p':[], '720p':[], '1080p':[]}
movie_name = group['library']['titles'][0]['title'] movie_name = getTitle(group['library'])
url = "%s?%s" % (self.url['backup'], tryUrlencode({'s':movie_name})) url = "%s?%s" % (self.url['backup'], tryUrlencode({'s':movie_name}))
data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url) data = self.getCache('hdtrailers.alt.%s' % group['library']['identifier'], url)

4
couchpotato/core/settings/__init__.py

@ -204,7 +204,7 @@ class Settings(object):
except: except:
pass pass
db.close() #db.close()
return prop return prop
def setProperty(self, identifier, value = ''): def setProperty(self, identifier, value = ''):
@ -221,4 +221,4 @@ class Settings(object):
p.value = toUnicode(value) p.value = toUnicode(value)
db.commit() db.commit()
db.close() #db.close()

3
couchpotato/environment.py

@ -12,7 +12,6 @@ class Env(object):
''' Environment variables ''' ''' Environment variables '''
_encoding = '' _encoding = ''
_uses_git = False
_debug = False _debug = False
_dev = False _dev = False
_settings = Settings() _settings = Settings()
@ -66,7 +65,7 @@ class Env(object):
@staticmethod @staticmethod
def getEngine(): def getEngine():
return create_engine(Env.get('db_path'), echo = False) return create_engine(Env.get('db_path'), echo = False, pool_recycle = 30)
@staticmethod @staticmethod
def setting(attr, section = 'core', value = None, default = '', type = None): def setting(attr, section = 'core', value = None, default = '', type = None):

5
couchpotato/runner.py

@ -28,8 +28,6 @@ def getOptions(base_path, args):
dest = 'console_log', help = "Log to console") dest = 'console_log', help = "Log to console")
parser.add_argument('--quiet', action = 'store_true', parser.add_argument('--quiet', action = 'store_true',
dest = 'quiet', help = 'No console logging') dest = 'quiet', help = 'No console logging')
parser.add_argument('--nogit', action = 'store_true',
dest = 'nogit', help = 'No git available')
parser.add_argument('--daemon', action = 'store_true', parser.add_argument('--daemon', action = 'store_true',
dest = 'daemon', help = 'Daemonize the app') dest = 'daemon', help = 'Daemonize the app')
parser.add_argument('--pid_file', default = os.path.join(data_dir, 'couchpotato.pid'), parser.add_argument('--pid_file', default = os.path.join(data_dir, 'couchpotato.pid'),
@ -93,7 +91,6 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Register environment settings # Register environment settings
Env.set('encoding', encoding) Env.set('encoding', encoding)
Env.set('uses_git', not options.nogit)
Env.set('app_dir', base_path) Env.set('app_dir', base_path)
Env.set('data_dir', data_dir) Env.set('data_dir', data_dir)
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log')) Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log'))
@ -132,7 +129,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Logger # Logger
logger = logging.getLogger() logger = logging.getLogger()
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%H:%M:%S') formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%m-%d %H:%M:%S')
level = logging.DEBUG if debug else logging.INFO level = logging.DEBUG if debug else logging.INFO
logger.setLevel(level) logger.setLevel(level)

7
libs/elixir/__init__.py

@ -38,7 +38,7 @@ from elixir.statements import Statement
from elixir.collection import EntityCollection, GlobalEntityCollection from elixir.collection import EntityCollection, GlobalEntityCollection
__version__ = '0.7.1' __version__ = '0.8.0dev'
__all__ = ['Entity', 'EntityBase', 'EntityMeta', 'EntityCollection', __all__ = ['Entity', 'EntityBase', 'EntityMeta', 'EntityCollection',
'entities', 'entities',
@ -85,11 +85,6 @@ def drop_all(*args, **kwargs):
def setup_all(create_tables=False, *args, **kwargs): def setup_all(create_tables=False, *args, **kwargs):
'''Setup the table and mapper of all entities in the default entity '''Setup the table and mapper of all entities in the default entity
collection. collection.
This is called automatically if any entity of the collection is configured
with the `autosetup` option and it is first accessed,
instanciated (called) or the create_all method of a metadata containing
tables from any of those entities is called.
''' '''
setup_entities(entities) setup_entities(entities)

4
libs/elixir/collection.py

@ -4,8 +4,6 @@ Default entity collection implementation
import sys import sys
import re import re
from elixir.py23compat import rsplit
class BaseCollection(list): class BaseCollection(list):
def __init__(self, entities=None): def __init__(self, entities=None):
list.__init__(self) list.__init__(self)
@ -24,7 +22,7 @@ class BaseCollection(list):
root = entity._descriptor.resolve_root root = entity._descriptor.resolve_root
if root: if root:
full_path = '%s.%s' % (root, full_path) full_path = '%s.%s' % (root, full_path)
module_path, classname = rsplit(full_path, '.', 1) module_path, classname = full_path.rsplit('.', 1)
module = sys.modules[module_path] module = sys.modules[module_path]
res = getattr(module, classname, None) res = getattr(module, classname, None)
if res is None: if res is None:

197
libs/elixir/entity.py

@ -3,8 +3,6 @@ This module provides the ``Entity`` base class, as well as its metaclass
``EntityMeta``. ``EntityMeta``.
''' '''
from py23compat import sorted
import sys import sys
import types import types
import warnings import warnings
@ -25,11 +23,6 @@ from elixir import options
from elixir.properties import Property from elixir.properties import Property
DEBUG = False DEBUG = False
try:
from sqlalchemy.orm import EXT_PASS
SA05orlater = False
except ImportError:
SA05orlater = True
__doc_all__ = ['Entity', 'EntityMeta'] __doc_all__ = ['Entity', 'EntityMeta']
@ -205,6 +198,7 @@ class EntityDescriptor(object):
parent_desc = self.parent._descriptor parent_desc = self.parent._descriptor
tablename = parent_desc.table_fullname tablename = parent_desc.table_fullname
join_clauses = [] join_clauses = []
fk_columns = []
for pk_col in parent_desc.primary_keys: for pk_col in parent_desc.primary_keys:
colname = options.MULTIINHERITANCECOL_NAMEFORMAT % \ colname = options.MULTIINHERITANCECOL_NAMEFORMAT % \
{'entity': self.parent.__name__.lower(), {'entity': self.parent.__name__.lower(),
@ -214,12 +208,14 @@ class EntityDescriptor(object):
# a real column object when said column is not yet # a real column object when said column is not yet
# attached to a table # attached to a table
pk_col_name = "%s.%s" % (tablename, pk_col.key) pk_col_name = "%s.%s" % (tablename, pk_col.key)
fk = ForeignKey(pk_col_name, ondelete='cascade') col = Column(colname, pk_col.type, primary_key=True)
col = Column(colname, pk_col.type, fk, fk_columns.append(col)
primary_key=True)
self.add_column(col) self.add_column(col)
join_clauses.append(col == pk_col) join_clauses.append(col == pk_col)
self.join_condition = and_(*join_clauses) self.join_condition = and_(*join_clauses)
self.add_constraint(
ForeignKeyConstraint(fk_columns,
parent_desc.primary_keys, ondelete='CASCADE'))
elif self.inheritance == 'concrete': elif self.inheritance == 'concrete':
# Copy primary key columns from the parent. # Copy primary key columns from the parent.
for col in self.parent._descriptor.columns: for col in self.parent._descriptor.columns:
@ -286,7 +282,7 @@ class EntityDescriptor(object):
self.add_constraint( self.add_constraint(
ForeignKeyConstraint( ForeignKeyConstraint(
[e.parent.key for e in con.elements], [e.parent.key for e in con.elements],
[e._get_colspec() for e in con.elements], [e.target_fullname for e in con.elements],
name=con.name, #TODO: modify it name=con.name, #TODO: modify it
onupdate=con.onupdate, ondelete=con.ondelete, onupdate=con.onupdate, ondelete=con.ondelete,
use_alter=con.use_alter)) use_alter=con.use_alter))
@ -370,6 +366,11 @@ class EntityDescriptor(object):
order = [] order = []
for colname in order_by: for colname in order_by:
#FIXME: get_column uses self.columns[key] instead of property
# names. self.columns correspond to the columns of the table if
# the table was already created and to self._columns otherwise,
# which is a ColumnCollection indexed on columns.key
# See ticket #108.
col = self.get_column(colname.strip('-')) col = self.get_column(colname.strip('-'))
if colname.startswith('-'): if colname.startswith('-'):
col = desc(col) col = desc(col)
@ -493,17 +494,13 @@ class EntityDescriptor(object):
(col.key, self.entity.__name__)) (col.key, self.entity.__name__))
else: else:
del self._columns[col.key] del self._columns[col.key]
# are indexed on col.key
self._columns.add(col) self._columns.add(col)
if col.primary_key: if col.primary_key:
self.has_pk = True self.has_pk = True
# Autosetup triggers shouldn't be active anymore at this point, so we table = self.entity.table
# can theoretically access the entity's table safely. But the problem
# is that if, for some reason, the trigger removal phase didn't
# happen, we'll get an infinite loop. So we just make sure we don't
# get one in any case.
table = type.__getattribute__(self.entity, 'table')
if table is not None: if table is not None:
if check_duplicate and col.key in table.columns.keys(): if check_duplicate and col.key in table.columns.keys():
raise Exception("Column '%s' already exist in table '%s' ! " % raise Exception("Column '%s' already exist in table '%s' ! " %
@ -595,6 +592,7 @@ class EntityDescriptor(object):
#------------------------ #------------------------
# some useful properties # some useful properties
@property
def table_fullname(self): def table_fullname(self):
''' '''
Complete name of the table for the related entity. Complete name of the table for the related entity.
@ -605,8 +603,8 @@ class EntityDescriptor(object):
return "%s.%s" % (schema, self.tablename) return "%s.%s" % (schema, self.tablename)
else: else:
return self.tablename return self.tablename
table_fullname = property(table_fullname)
@property
def columns(self): def columns(self):
if self.entity.table is not None: if self.entity.table is not None:
return self.entity.table.columns return self.entity.table.columns
@ -615,8 +613,8 @@ class EntityDescriptor(object):
# return the parent entity's columns (for example for order_by # return the parent entity's columns (for example for order_by
# using a column defined in the parent. # using a column defined in the parent.
return self._columns return self._columns
columns = property(columns)
@property
def primary_keys(self): def primary_keys(self):
""" """
Returns the list of primary key columns of the entity. Returns the list of primary key columns of the entity.
@ -630,15 +628,15 @@ class EntityDescriptor(object):
return self.parent._descriptor.primary_keys return self.parent._descriptor.primary_keys
else: else:
return [col for col in self.columns if col.primary_key] return [col for col in self.columns if col.primary_key]
primary_keys = property(primary_keys)
@property
def table(self): def table(self):
if self.entity.table is not None: if self.entity.table is not None:
return self.entity.table return self.entity.table
else: else:
return FakeTable(self) return FakeTable(self)
table = property(table)
@property
def primary_key_properties(self): def primary_key_properties(self):
""" """
Returns the list of (mapper) properties corresponding to the primary Returns the list of (mapper) properties corresponding to the primary
@ -653,30 +651,32 @@ class EntityDescriptor(object):
for prop in mapper.iterate_properties: for prop in mapper.iterate_properties:
if isinstance(prop, ColumnProperty): if isinstance(prop, ColumnProperty):
for col in prop.columns: for col in prop.columns:
#XXX: Why is this extra loop necessary? What is this
# "proxy_set" supposed to mean?
for col in col.proxy_set: for col in col.proxy_set:
col_to_prop[col] = prop col_to_prop[col] = prop
pk_cols = [c for c in mapper.mapped_table.c if c.primary_key] pk_cols = [c for c in mapper.mapped_table.c if c.primary_key]
self._pk_props = [col_to_prop[c] for c in pk_cols] self._pk_props = [col_to_prop[c] for c in pk_cols]
return self._pk_props return self._pk_props
primary_key_properties = property(primary_key_properties)
class FakePK(object): class FakePK(object):
def __init__(self, descriptor): def __init__(self, descriptor):
self.descriptor = descriptor self.descriptor = descriptor
@property
def columns(self): def columns(self):
return self.descriptor.primary_keys return self.descriptor.primary_keys
columns = property(columns)
class FakeTable(object): class FakeTable(object):
def __init__(self, descriptor): def __init__(self, descriptor):
self.descriptor = descriptor self.descriptor = descriptor
self.primary_key = FakePK(descriptor) self.primary_key = FakePK(descriptor)
@property
def columns(self): def columns(self):
return self.descriptor.columns return self.descriptor.columns
columns = property(columns)
@property
def fullname(self): def fullname(self):
''' '''
Complete name of the table for the related entity. Complete name of the table for the related entity.
@ -687,45 +687,7 @@ class FakeTable(object):
return "%s.%s" % (schema, self.descriptor.tablename) return "%s.%s" % (schema, self.descriptor.tablename)
else: else:
return self.descriptor.tablename return self.descriptor.tablename
fullname = property(fullname)
class TriggerProxy(object):
"""
A class that serves as a "trigger" ; accessing its attributes runs
the setup_all function.
Note that the `setup_all` is called on each access of the attribute.
"""
def __init__(self, class_, attrname):
self.class_ = class_
self.attrname = attrname
def __getattr__(self, name):
elixir.setup_all()
#FIXME: it's possible to get an infinite loop here if setup_all doesn't
#remove the triggers for this entity. This can happen if the entity is
#not in the `entities` list for some reason.
proxied_attr = getattr(self.class_, self.attrname)
return getattr(proxied_attr, name)
def __repr__(self):
proxied_attr = getattr(self.class_, self.attrname)
return "<TriggerProxy (%s)>" % (self.class_.__name__)
class TriggerAttribute(object):
def __init__(self, attrname):
self.attrname = attrname
def __get__(self, instance, owner):
#FIXME: it's possible to get an infinite loop here if setup_all doesn't
#remove the triggers for this entity. This can happen if the entity is
#not in the `entities` list for some reason.
elixir.setup_all()
return getattr(owner, self.attrname)
def is_entity(cls): def is_entity(cls):
""" """
@ -805,13 +767,6 @@ def instrument_class(cls):
# setup misc options here (like tablename etc.) # setup misc options here (like tablename etc.)
desc.setup_options() desc.setup_options()
# create trigger proxies
# TODO: support entity_name... It makes sense only for autoloaded
# tables for now, and would make more sense if we support "external"
# tables
if desc.autosetup:
_install_autosetup_triggers(cls)
class EntityMeta(type): class EntityMeta(type):
""" """
@ -823,11 +778,6 @@ class EntityMeta(type):
def __init__(cls, name, bases, dict_): def __init__(cls, name, bases, dict_):
instrument_class(cls) instrument_class(cls)
def __call__(cls, *args, **kwargs):
if cls._descriptor.autosetup and not hasattr(cls, '_setup_done'):
elixir.setup_all()
return type.__call__(cls, *args, **kwargs)
def __setattr__(cls, key, value): def __setattr__(cls, key, value):
if isinstance(value, Property): if isinstance(value, Property):
if hasattr(cls, '_setup_done'): if hasattr(cls, '_setup_done'):
@ -839,84 +789,6 @@ class EntityMeta(type):
type.__setattr__(cls, key, value) type.__setattr__(cls, key, value)
def _install_autosetup_triggers(cls, entity_name=None):
#TODO: move as much as possible of those "_private" values to the
# descriptor, so that we don't mess the initial class.
warnings.warn("The 'autosetup' option on entities is deprecated. "
"Please call setup_all() manually after all your entities have been "
"declared.", DeprecationWarning, stacklevel=4)
tablename = cls._descriptor.tablename
schema = cls._descriptor.table_options.get('schema', None)
cls._table_key = sqlalchemy.schema._get_table_key(tablename, schema)
table_proxy = TriggerProxy(cls, 'table')
md = cls._descriptor.metadata
md.tables[cls._table_key] = table_proxy
# We need to monkeypatch the metadata's table iterator method because
# otherwise it doesn't work if the setup is triggered by the
# metadata.create_all().
# This is because ManyToMany relationships add tables AFTER the list
# of tables that are going to be created is "computed"
# (metadata.tables.values()).
# see:
# - table_iterator method in MetaData class in sqlalchemy/schema.py
# - visit_metadata method in sqlalchemy/ansisql.py
if SA05orlater:
warnings.warn(
"The automatic setup via metadata.create_all() through "
"the autosetup option doesn't work with SQLAlchemy 0.5 and later!")
else:
# SA 0.6 does not use table_iterator anymore (it was already deprecated
# since SA 0.5.0)
original_table_iterator = md.table_iterator
if not hasattr(original_table_iterator,
'_non_elixir_patched_iterator'):
def table_iterator(*args, **kwargs):
elixir.setup_all()
return original_table_iterator(*args, **kwargs)
table_iterator.__doc__ = original_table_iterator.__doc__
table_iterator._non_elixir_patched_iterator = \
original_table_iterator
md.table_iterator = table_iterator
#TODO: we might want to add all columns that will be available as
#attributes on the class itself (in SA 0.4+). This is a pretty
#rare usecase, as people will normally hit the query attribute before the
#column attributes, but I've seen people hitting this problem...
for name in ('c', 'table', 'mapper', 'query'):
setattr(cls, name, TriggerAttribute(name))
cls._has_triggers = True
def _cleanup_autosetup_triggers(cls):
if not hasattr(cls, '_has_triggers'):
return
for name in ('table', 'mapper'):
setattr(cls, name, None)
for name in ('c', 'query'):
delattr(cls, name)
desc = cls._descriptor
md = desc.metadata
# the fake table could have already been removed (namely in a
# single table inheritance scenario)
md.tables.pop(cls._table_key, None)
# restore original table iterator if not done already
if not SA05orlater:
if hasattr(md.table_iterator, '_non_elixir_patched_iterator'):
md.table_iterator = \
md.table_iterator._non_elixir_patched_iterator
del cls._has_triggers
def setup_entities(entities): def setup_entities(entities):
'''Setup all entities in the list passed as argument''' '''Setup all entities in the list passed as argument'''
@ -928,9 +800,6 @@ def setup_entities(entities):
if isinstance(attr, Property): if isinstance(attr, Property):
delattr(entity, name) delattr(entity, name)
if entity._descriptor.autosetup:
_cleanup_autosetup_triggers(entity)
for method_name in ( for method_name in (
'setup_autoload_table', 'create_pk_cols', 'setup_relkeys', 'setup_autoload_table', 'create_pk_cols', 'setup_relkeys',
'before_table', 'setup_table', 'setup_reltables', 'after_table', 'before_table', 'setup_table', 'setup_reltables', 'after_table',
@ -955,8 +824,7 @@ def setup_entities(entities):
def cleanup_entities(entities): def cleanup_entities(entities):
""" """
Try to revert back the list of entities passed as argument to the state Try to revert back the list of entities passed as argument to the state
they had just before their setup phase. It will not work entirely for they had just before their setup phase.
autosetup entities as we need to remove the autosetup triggers.
As of now, this function is *not* functional in that it doesn't revert to As of now, this function is *not* functional in that it doesn't revert to
the exact same state the entities were before setup. For example, the the exact same state the entities were before setup. For example, the
@ -968,8 +836,6 @@ def cleanup_entities(entities):
""" """
for entity in entities: for entity in entities:
desc = entity._descriptor desc = entity._descriptor
if desc.autosetup:
_cleanup_autosetup_triggers(entity)
if hasattr(entity, '_setup_done'): if hasattr(entity, '_setup_done'):
del entity._setup_done del entity._setup_done
@ -1007,6 +873,7 @@ class EntityBase(object):
for key, value in kwargs.iteritems(): for key, value in kwargs.iteritems():
setattr(self, key, value) setattr(self, key, value)
@classmethod
def update_or_create(cls, data, surrogate=True): def update_or_create(cls, data, surrogate=True):
pk_props = cls._descriptor.primary_key_properties pk_props = cls._descriptor.primary_key_properties
@ -1016,17 +883,16 @@ class EntityBase(object):
record = cls.query.get(pk_tuple) record = cls.query.get(pk_tuple)
if record is None: if record is None:
if surrogate: if surrogate:
raise Exception("cannot create surrogate with pk") raise Exception("Cannot create surrogate with pk")
else: else:
record = cls() record = cls()
else: else:
if surrogate: if surrogate:
record = cls() record = cls()
else: else:
raise Exception("cannot create non surrogate without pk") raise Exception("Cannot create non surrogate without pk")
record.from_dict(data) record.from_dict(data)
return record return record
update_or_create = classmethod(update_or_create)
def from_dict(self, data): def from_dict(self, data):
""" """
@ -1105,10 +971,11 @@ class EntityBase(object):
# This bunch of session methods, along with all the query methods below # This bunch of session methods, along with all the query methods below
# only make sense when using a global/scoped/contextual session. # only make sense when using a global/scoped/contextual session.
@property
def _global_session(self): def _global_session(self):
return self._descriptor.session.registry() return self._descriptor.session.registry()
_global_session = property(_global_session)
#FIXME: remove all deprecated methods, possibly all of these
def merge(self, *args, **kwargs): def merge(self, *args, **kwargs):
return self._global_session.merge(self, *args, **kwargs) return self._global_session.merge(self, *args, **kwargs)
@ -1126,6 +993,7 @@ class EntityBase(object):
return self._global_session.save_or_update(self, *args, **kwargs) return self._global_session.save_or_update(self, *args, **kwargs)
# query methods # query methods
@classmethod
def get_by(cls, *args, **kwargs): def get_by(cls, *args, **kwargs):
""" """
Returns the first instance of this class matching the given criteria. Returns the first instance of this class matching the given criteria.
@ -1133,8 +1001,8 @@ class EntityBase(object):
session.query(MyClass).filter_by(...).first() session.query(MyClass).filter_by(...).first()
""" """
return cls.query.filter_by(*args, **kwargs).first() return cls.query.filter_by(*args, **kwargs).first()
get_by = classmethod(get_by)
@classmethod
def get(cls, *args, **kwargs): def get(cls, *args, **kwargs):
""" """
Return the instance of this class based on the given identifier, Return the instance of this class based on the given identifier,
@ -1142,7 +1010,6 @@ class EntityBase(object):
session.query(MyClass).get(...) session.query(MyClass).get(...)
""" """
return cls.query.get(*args, **kwargs) return cls.query.get(*args, **kwargs)
get = classmethod(get)
class Entity(EntityBase): class Entity(EntityBase):

9
libs/elixir/events.py

@ -1,3 +1,5 @@
from sqlalchemy.orm import reconstructor
__all__ = [ __all__ = [
'before_insert', 'before_insert',
'after_insert', 'after_insert',
@ -22,9 +24,4 @@ before_update = create_decorator('before_update')
after_update = create_decorator('after_update') after_update = create_decorator('after_update')
before_delete = create_decorator('before_delete') before_delete = create_decorator('before_delete')
after_delete = create_decorator('after_delete') after_delete = create_decorator('after_delete')
try:
from sqlalchemy.orm import reconstructor
except ImportError:
def reconstructor(func):
raise Exception('The reconstructor method decorator is only '
'available with SQLAlchemy 0.5 and later')

4
libs/elixir/ext/associable.py

@ -222,12 +222,12 @@ def associable(assoc_entity, plural_name=None, lazy=True):
# add helper methods # add helper methods
def select_by(cls, **kwargs): def select_by(cls, **kwargs):
return cls.query.join([attr_name, 'targets']) \ return cls.query.join(attr_name, 'targets') \
.filter_by(**kwargs).all() .filter_by(**kwargs).all()
setattr(entity, 'select_by_%s' % self.name, classmethod(select_by)) setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
def select(cls, *args, **kwargs): def select(cls, *args, **kwargs):
return cls.query.join([attr_name, 'targets']) \ return cls.query.join(attr_name, 'targets') \
.filter(*args, **kwargs).all() .filter(*args, **kwargs).all()
setattr(entity, 'select_%s' % self.name, classmethod(select)) setattr(entity, 'select_%s' % self.name, classmethod(select))

251
libs/elixir/ext/list.py

@ -1,251 +0,0 @@
'''
This extension is DEPRECATED. Please use the orderinglist SQLAlchemy
extension instead.
For details:
http://www.sqlalchemy.org/docs/05/reference/ext/orderinglist.html
For an Elixir example:
http://elixir.ematia.de/trac/wiki/Recipes/UsingEntityForOrderedList
or
http://elixir.ematia.de/trac/browser/elixir/0.7.0/tests/test_o2m.py#L155
An ordered-list plugin for Elixir to help you make an entity be able to be
managed in a list-like way. Much inspiration comes from the Ruby on Rails
acts_as_list plugin, which is currently more full-featured than this plugin.
Once you flag an entity with an `acts_as_list()` statement, a column will be
added to the entity called `position` which will be an integer column that is
managed for you by the plugin. You can pass an alternative column name to
the plugin using the `column_name` keyword argument.
In addition, your entity will get a series of new methods attached to it,
including:
+----------------------+------------------------------------------------------+
| Method Name | Description |
+======================+======================================================+
| ``move_lower`` | Move the item lower in the list |
+----------------------+------------------------------------------------------+
| ``move_higher`` | Move the item higher in the list |
+----------------------+------------------------------------------------------+
| ``move_to_bottom`` | Move the item to the bottom of the list |
+----------------------+------------------------------------------------------+
| ``move_to_top`` | Move the item to the top of the list |
+----------------------+------------------------------------------------------+
| ``move_to`` | Move the item to a specific position in the list |
+----------------------+------------------------------------------------------+
Sometimes, your entities that represent list items will be a part of different
lists. To implement this behavior, simply pass the `acts_as_list` statement a
callable that returns a "qualifier" SQLAlchemy expression. This expression will
be added to the generated WHERE clauses used by the plugin.
Example model usage:
.. sourcecode:: python
from elixir import *
from elixir.ext.list import acts_as_list
class ToDo(Entity):
subject = Field(String(128))
owner = ManyToOne('Person')
def qualify(self):
return ToDo.owner_id == self.owner_id
acts_as_list(qualifier=qualify)
class Person(Entity):
name = Field(String(64))
todos = OneToMany('ToDo', order_by='position')
The above example can then be used to manage ordered todo lists for people.
Note that you must set the `order_by` property on the `Person.todo` relation in
order for the relation to respect the ordering. Here is an example of using
this model in practice:
.. sourcecode:: python
p = Person.query.filter_by(name='Jonathan').one()
p.todos.append(ToDo(subject='Three'))
p.todos.append(ToDo(subject='Two'))
p.todos.append(ToDo(subject='One'))
session.commit(); session.clear()
p = Person.query.filter_by(name='Jonathan').one()
p.todos[0].move_to_bottom()
p.todos[2].move_to_top()
session.commit(); session.clear()
p = Person.query.filter_by(name='Jonathan').one()
assert p.todos[0].subject == 'One'
assert p.todos[1].subject == 'Two'
assert p.todos[2].subject == 'Three'
For more examples, refer to the unit tests for this plugin.
'''
from elixir.statements import Statement
from elixir.events import before_insert, before_delete
from sqlalchemy import Column, Integer, select, func, literal, and_
import warnings
__all__ = ['acts_as_list']
__doc_all__ = []
def get_entity_where(instance):
clauses = []
for column in instance.table.primary_key.columns:
instance_value = getattr(instance, column.name)
clauses.append(column == instance_value)
return and_(*clauses)
class ListEntityBuilder(object):
def __init__(self, entity, qualifier=None, column_name='position'):
warnings.warn("The act_as_list extension is deprecated. Please use "
"SQLAlchemy's orderinglist extension instead",
DeprecationWarning, stacklevel=6)
self.entity = entity
self.qualifier_method = qualifier
self.column_name = column_name
def create_non_pk_cols(self):
if self.entity._descriptor.autoload:
for c in self.entity.table.c:
if c.name == self.column_name:
self.position_column = c
if not hasattr(self, 'position_column'):
raise Exception(
"Could not find column '%s' in autoloaded table '%s', "
"needed by entity '%s'." % (self.column_name,
self.entity.table.name, self.entity.__name__))
else:
self.position_column = Column(self.column_name, Integer)
self.entity._descriptor.add_column(self.position_column)
def after_table(self):
position_column = self.position_column
position_column_name = self.column_name
qualifier_method = self.qualifier_method
if not qualifier_method:
qualifier_method = lambda self: None
def _init_position(self):
s = select(
[(func.max(position_column)+1).label('value')],
qualifier_method(self)
).union(
select([literal(1).label('value')])
)
a = s.alias()
# we use a second func.max to get the maximum between 1 and the
# real max position if any exist
setattr(self, position_column_name, select([func.max(a.c.value)]))
# Note that this method could be rewritten more simply like below,
# but because this extension is going to be deprecated anyway,
# I don't want to risk breaking something I don't want to maintain.
# setattr(self, position_column_name, select(
# [func.coalesce(func.max(position_column), 0) + 1],
# qualifier_method(self)
# ))
_init_position = before_insert(_init_position)
def _shift_items(self):
self.table.update(
and_(
position_column > getattr(self, position_column_name),
qualifier_method(self)
),
values={
position_column : position_column - 1
}
).execute()
_shift_items = before_delete(_shift_items)
def move_to_bottom(self):
# move the items that were above this item up one
self.table.update(
and_(
position_column >= getattr(self, position_column_name),
qualifier_method(self)
),
values = {
position_column : position_column - 1
}
).execute()
# move this item to the max position
# MySQL does not support the correlated subquery, so we need to
# execute the query (through scalar()). See ticket #34.
self.table.update(
get_entity_where(self),
values={
position_column : select(
[func.max(position_column) + 1],
qualifier_method(self)
).scalar()
}
).execute()
def move_to_top(self):
self.move_to(1)
def move_to(self, position):
current_position = getattr(self, position_column_name)
# determine which direction we're moving
if position < current_position:
where = and_(
position <= position_column,
position_column < current_position,
qualifier_method(self)
)
modifier = 1
elif position > current_position:
where = and_(
current_position < position_column,
position_column <= position,
qualifier_method(self)
)
modifier = -1
# shift the items in between the current and new positions
self.table.update(where, values = {
position_column : position_column + modifier
}).execute()
# update this item's position to the desired position
self.table.update(get_entity_where(self)) \
.execute(**{position_column_name: position})
def move_lower(self):
# replace for ex.: p.todos.insert(x + 1, p.todos.pop(x))
self.move_to(getattr(self, position_column_name) + 1)
def move_higher(self):
self.move_to(getattr(self, position_column_name) - 1)
# attach new methods to entity
self.entity._init_position = _init_position
self.entity._shift_items = _shift_items
self.entity.move_lower = move_lower
self.entity.move_higher = move_higher
self.entity.move_to_bottom = move_to_bottom
self.entity.move_to_top = move_to_top
self.entity.move_to = move_to
acts_as_list = Statement(ListEntityBuilder)

11
libs/elixir/options.py

@ -124,16 +124,6 @@ The list of supported arguments are as follows:
| | module by setting the ``__session__`` attribute of | | | module by setting the ``__session__`` attribute of |
| | that module. | | | that module. |
+---------------------+-------------------------------------------------------+ +---------------------+-------------------------------------------------------+
| ``autosetup`` | DEPRECATED. Specify whether that entity will contain |
| | automatic setup triggers. |
| | That is if this entity will be |
| | automatically setup (along with all other entities |
| | which were already declared) if any of the following |
| | condition happen: some of its attributes are accessed |
| | ('c', 'table', 'mapper' or 'query'), instanciated |
| | (called) or the create_all method of this entity's |
| | metadata is called. Defaults to ``False``. |
+---------------------+-------------------------------------------------------+
| ``allowcoloverride``| Specify whether it is allowed to override columns. | | ``allowcoloverride``| Specify whether it is allowed to override columns. |
| | By default, Elixir forbids you to add a column to an | | | By default, Elixir forbids you to add a column to an |
| | entity's table which already exist in that table. If | | | entity's table which already exist in that table. If |
@ -225,7 +215,6 @@ MIGRATION_TO_07_AID = False
# #
options_defaults = dict( options_defaults = dict(
abstract=False, abstract=False,
autosetup=False,
inheritance='single', inheritance='single',
polymorphic=True, polymorphic=True,
identity=None, identity=None,

73
libs/elixir/py23compat.py

@ -1,73 +0,0 @@
# Some helper functions to get by without Python 2.4
# set
try:
set = set
except NameError:
from sets import Set as set
orig_cmp = cmp
# [].sort
def sort_list(l, cmp=None, key=None, reverse=False):
try:
l.sort(cmp, key, reverse)
except TypeError, e:
if not str(e).startswith('sort expected at most 1 arguments'):
raise
if cmp is None:
cmp = orig_cmp
if key is not None:
# the cmp=cmp parameter is required to get the original comparator
# into the lambda namespace
cmp = lambda self, other, cmp=cmp: cmp(key(self), key(other))
if reverse:
cmp = lambda self, other, cmp=cmp: -cmp(self,other)
l.sort(cmp)
# sorted
try:
sorted = sorted
except NameError:
# global name 'sorted' doesn't exist in Python2.3
# this provides a poor-man's emulation of the sorted built-in method
def sorted(l, cmp=None, key=None, reverse=False):
sorted_list = list(l)
sort_list(sorted_list, cmp, key, reverse)
return sorted_list
# rsplit
try:
''.rsplit
def rsplit(s, delim, maxsplit):
return s.rsplit(delim, maxsplit)
except AttributeError:
def rsplit(s, delim, maxsplit):
"""Return a list of the words of the string s, scanning s
from the end. To all intents and purposes, the resulting
list of words is the same as returned by split(), except
when the optional third argument maxsplit is explicitly
specified and nonzero. When maxsplit is nonzero, at most
maxsplit number of splits - the rightmost ones - occur,
and the remainder of the string is returned as the first
element of the list (thus, the list will have at most
maxsplit+1 elements). New in version 2.4.
>>> rsplit('foo.bar.baz', '.', 0)
['foo.bar.baz']
>>> rsplit('foo.bar.baz', '.', 1)
['foo.bar', 'baz']
>>> rsplit('foo.bar.baz', '.', 2)
['foo', 'bar', 'baz']
>>> rsplit('foo.bar.baz', '.', 99)
['foo', 'bar', 'baz']
"""
assert maxsplit >= 0
if maxsplit == 0: return [s]
# the following lines perform the function, but inefficiently.
# This may be adequate for compatibility purposes
items = s.split(delim)
if maxsplit < len(items):
items[:-maxsplit] = [delim.join(items[:-maxsplit])]
return items

50
libs/elixir/relationships.py

@ -278,8 +278,9 @@ relationships accept the following optional (keyword) arguments:
| | reference the "local"/current entity's table. | | | reference the "local"/current entity's table. |
+--------------------+--------------------------------------------------------+ +--------------------+--------------------------------------------------------+
| ``table`` | Use a manually created table. If this argument is | | ``table`` | Use a manually created table. If this argument is |
| | used, Elixir won't generate a table for this | | | used, Elixir will not generate a table for this |
| | relationship, and use the one given instead. | | | relationship, and use the one given instead. This |
| | argument only accepts SQLAlchemy's Table objects. |
+--------------------+--------------------------------------------------------+ +--------------------+--------------------------------------------------------+
| ``order_by`` | Specify which field(s) should be used to sort the | | ``order_by`` | Specify which field(s) should be used to sort the |
| | results given by accessing the relation field. | | | results given by accessing the relation field. |
@ -303,13 +304,6 @@ relationships accept the following optional (keyword) arguments:
| ``table_kwargs`` | A dictionary holding any other keyword argument you | | ``table_kwargs`` | A dictionary holding any other keyword argument you |
| | might want to pass to the underlying Table object. | | | might want to pass to the underlying Table object. |
+--------------------+--------------------------------------------------------+ +--------------------+--------------------------------------------------------+
| ``column_format`` | DEPRECATED. Specify an alternate format string for |
| | naming the |
| | columns in the mapping table. The default value is |
| | defined in ``elixir.options.M2MCOL_NAMEFORMAT``. You |
| | will be passed ``tablename``, ``key``, and ``entity`` |
| | as arguments to the format string. |
+--------------------+--------------------------------------------------------+
================ ================
@ -493,6 +487,7 @@ class Relationship(Property):
self.property = relation(self.target, **kwargs) self.property = relation(self.target, **kwargs)
self.add_mapper_property(self.name, self.property) self.add_mapper_property(self.name, self.property)
@property
def target(self): def target(self):
if not self._target: if not self._target:
if isinstance(self.of_kind, basestring): if isinstance(self.of_kind, basestring):
@ -501,8 +496,8 @@ class Relationship(Property):
else: else:
self._target = self.of_kind self._target = self.of_kind
return self._target return self._target
target = property(target)
@property
def inverse(self): def inverse(self):
if not hasattr(self, '_inverse'): if not hasattr(self, '_inverse'):
if self.inverse_name: if self.inverse_name:
@ -531,7 +526,6 @@ class Relationship(Property):
inverse._inverse = self inverse._inverse = self
return self._inverse return self._inverse
inverse = property(inverse)
def match_type_of(self, other): def match_type_of(self, other):
return False return False
@ -612,12 +606,12 @@ class ManyToOne(Relationship):
def match_type_of(self, other): def match_type_of(self, other):
return isinstance(other, (OneToMany, OneToOne)) return isinstance(other, (OneToMany, OneToOne))
@property
def target_table(self): def target_table(self):
if isinstance(self.target, EntityMeta): if isinstance(self.target, EntityMeta):
return self.target._descriptor.table return self.target._descriptor.table
else: else:
return class_mapper(self.target).local_table return class_mapper(self.target).local_table
target_table = property(target_table)
def create_keys(self, pk): def create_keys(self, pk):
''' '''
@ -634,7 +628,10 @@ class ManyToOne(Relationship):
source_desc = self.entity._descriptor source_desc = self.entity._descriptor
if isinstance(self.target, EntityMeta): if isinstance(self.target, EntityMeta):
# make sure the target has all its pk set up # make sure the target has all its pk set up
#FIXME: this is not enough when specifying target_column manually,
# on unique, non-pk col, see tests/test_m2o.py:test_non_pk_forward
self.target._descriptor.create_pk_cols() self.target._descriptor.create_pk_cols()
#XXX: another option, instead of the FakeTable, would be to create an #XXX: another option, instead of the FakeTable, would be to create an
# EntityDescriptor for the SA class. # EntityDescriptor for the SA class.
target_table = self.target_table target_table = self.target_table
@ -655,6 +652,12 @@ class ManyToOne(Relationship):
"Couldn't find a foreign key constraint in table " "Couldn't find a foreign key constraint in table "
"'%s' using the following columns: %s." "'%s' using the following columns: %s."
% (self.entity.table.name, colnames)) % (self.entity.table.name, colnames))
else:
# in this case we let SA handle everything.
# XXX: we might want to try to build join clauses anyway so
# that we know whether there is an ambiguity or not, and
# suggest using colname if there is one
pass
if self.field: if self.field:
raise NotImplementedError( raise NotImplementedError(
"'field' argument not allowed on autoloaded table " "'field' argument not allowed on autoloaded table "
@ -808,8 +811,8 @@ class OneToOne(Relationship):
# useless because the remote_side is already setup in the other way # useless because the remote_side is already setup in the other way
# (ManyToOne). # (ManyToOne).
if self.entity.table is self.target.table: if self.entity.table is self.target.table:
#FIXME: IF this code is of any use, it will probably break for # When using a manual/autoloaded table, it will be assigned
# autoloaded tables # an empty list, which doesn't seem to upset SQLAlchemy
kwargs['remote_side'] = self.inverse.foreign_key kwargs['remote_side'] = self.inverse.foreign_key
# Contrary to ManyToMany relationships, we need to specify the join # Contrary to ManyToMany relationships, we need to specify the join
@ -839,7 +842,6 @@ class ManyToMany(Relationship):
local_colname=None, remote_colname=None, local_colname=None, remote_colname=None,
ondelete=None, onupdate=None, ondelete=None, onupdate=None,
table=None, schema=None, table=None, schema=None,
column_format=None,
filter=None, filter=None,
table_kwargs=None, table_kwargs=None,
*args, **kwargs): *args, **kwargs):
@ -858,14 +860,9 @@ class ManyToMany(Relationship):
self.table = table self.table = table
self.schema = schema self.schema = schema
if column_format: #TODO: this can probably be simplified/moved elsewhere since the
warnings.warn("The 'column_format' argument on ManyToMany " #argument disappeared
"relationships is deprecated. Please use the 'local_colname' " self.column_format = options.M2MCOL_NAMEFORMAT
"and/or 'remote_colname' arguments if you want custom "
"column names for this table only, or modify "
"options.M2MCOL_NAMEFORMAT if you want a custom format for "
"all ManyToMany tables", DeprecationWarning, stacklevel=3)
self.column_format = column_format or options.M2MCOL_NAMEFORMAT
if not hasattr(self.column_format, '__call__'): if not hasattr(self.column_format, '__call__'):
# we need to store the format in a variable so that the # we need to store the format in a variable so that the
# closure of the lambda is correct # closure of the lambda is correct
@ -891,13 +888,6 @@ class ManyToMany(Relationship):
super(ManyToMany, self).__init__(of_kind, *args, **kwargs) super(ManyToMany, self).__init__(of_kind, *args, **kwargs)
def get_table(self):
warnings.warn("The secondary_table attribute on ManyToMany objects is "
"deprecated. You should rather use the table attribute.",
DeprecationWarning, stacklevel=2)
return self.table
secondary_table = property(get_table)
def match_type_of(self, other): def match_type_of(self, other):
return isinstance(other, ManyToMany) return isinstance(other, ManyToMany)

Loading…
Cancel
Save